2025. 5. 28. 23:11ㆍJava/Android
기존에 진행 중이던 프로젝트 "FlipMarket"을 안드로이드 버전으로도 개발하면 괜찮겠다고 생각해서 웹 개발과 병행하며 진행 중에 있습니다.
솔직히 이 프로젝트는 할 필요 없었는데, 곧 선발전 대회가 있어서 대비할 겸 공부하자 생각해서 진행하게 되었습니다.
백엔드 설정
1. 필요한 의존성 추가 및 DB 연결
Spring Security + JWT를 활용해서 로그인을 구현할 것이므로 이와 관련된 라이브러리 의존성을 추가했습니다. 그 외의 것들은 이 후에 개발할 때 편의를 위함과 기본적인 라이브러리들을 추가했습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'org.springframework.boot:spring-boot-starter-security'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // or jjwt-gson
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
DB 연결의 경우 MySQL을 사용해서 진행할 것이고, 이에 따른 driver 설정 및 url 설정을 application.properties에 하였습니다.
spring.datasource.url 부분에 localhost:3306/StudyProject?... 쪽의 StudyProject는 연결할 스키마의 명칭이므로 다르게 설정하셨을 경우 그에 맞게 수정하셔야 합니다.
spring.application.name=FlipMarketAndroidBackend
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/StudyProject?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=1234
#true 설정 시 jpa 쿼리문 확인 가능
spring.jpa.show-sql=true
#DDL 정의시 DB의 고유 기능을 사용 가능
spring.jpa.hibernate.ddl-auto=update
# JPA의 구현체인 Hibernate가 동작하면서 발생한 SQL의 가독성을 높여줌
spring.jpa.properties.hibernate.format_sql=true
# 트랜잭션이 끝날 경우 connection이 반납될 수 있도록 설정
spring.jpa.open-in-view=false
2. 엔티티 추가
기본적으로 PK값을 넣고, 이메일, 비밀번호, 이름, 나이, 전화번호 값을 가지고 있습니다. 이 외의 role 컬럼은 유저/어드민 분류를 위해 추가하였고, createdAt, updatedAt은 회원가입 및 정보 수정 이후 언제 해당 이벤트가 이루어졌는지를 저장하기 위해 추가했습니다.
import java.time.LocalDateTime;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long num;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String pwd;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private int age;
private String phoneNum;
private String role;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
public User() {
// TODO Auto-generated constructor stub
}
public User(String email, String pwd, String name, int age, String phoneNum, String role) {
this.email = email;
this.pwd = pwd;
this.name = name;
this.age = age;
this.phoneNum = phoneNum;
this.role = role;
}
public Long getNum() {
return num;
}
public String getEmail() {
return email;
}
public String getPwd() {
return pwd;
}
public String getPhoneNum() {
return phoneNum;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public String getRole() {
return role;
}
}
3. User 엔티티 Repository 추가
로그인, 회원가입, 회원 조회 및 수정 등등 사용자와 관련된 DB 작업들을 담당할 Repository를 추가합니다.
findByEmail 메서드는 Email(아이디) 값을 통해 유저 정보를 조회하는 쿼리입니다. 후에 로그인 처리 시 사용할 쿼리입니다.
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.NativeQuery;
import org.springframework.stereotype.Repository;
import com.lgh.FlipMarketBackend.entity.User;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@NativeQuery("SELECT * FROM user WHERE email = ?1")
Optional<User> findByEmail(String email);
}
4. JWT 필터 및 인증 설정 파일 추가
Spring Security를 사용하게 되면 모든 인증이 제한됩니다. 그렇기 때문에 이후에 api url 요청할 때 접근을 실패하여 정상 작동이 안될 수도 있습니다. 이를 방지할 파일 하나를 추가했습니다.
"/api/android/login" url을 접근 허용하였는데, 해당 url은 추후에 프론트에서 요청할 url입니다. 미리 설정해놨습니다
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig { // JWT 필터 및 인증 설정 담당
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable()).cors(cors -> {
}).authorizeHttpRequests(
auth -> auth.requestMatchers("/api/android/login").permitAll().anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5. CORS 설정 파일 추가
Android <-> Spring Boot 간 API 요청을 자유롭게 해줄 수 있도록 CORS를 설정합니다.
CORS가 뭐냐면 다른 곳에서 현재 실행 중인 애플리케이션에 요청을 보내는 것에 대해 허용해줄 수 있게 하는 정책입니다.
ex) localhost:1234 에서 localhost:8080 로 request 할 경우 CORS 설정이 되어있지 않으면 기본적으로 요청을 거부함.
이걸 허용해야하만 하는 이유는, 지금 Android 앱에서 Boot 서버로 요청을 보내는 상황인데, 그렇게 되면 CORS 정책 상 요청을 거부하게 됩니다. 그렇게 되면 로그인 시 서버 쪽에서 거부하여 계속해서 실패 값을 반환할 수도 있습니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*").allowedMethods("*");
}
}
6. 토큰 생성 및 검증 파일 추가
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
@Service
public class JwtService { // 토큰 생성 및 검증을 담당
// JWT 서명을 위한 비밀 키 *실제 서버 운영 시에는 .properties 파일에 따로 정의해야함
private final String secretKey = "VoNP8e8GvbCPMJoXeZSCaHUSvJFk2Mav";
public String generatedToken(String username) {
SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date()) // 토큰 발급 시간 (현재 시간)
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 토큰 발급 시간은 1시간 유효로 설정
.signWith(key, SignatureAlgorithm.HS256) // 서명 알고리즘은 SHA-256 알고리즘 사용
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey.getBytes()) // SigningKey를 지정해야 위조되지 않은 토큰임을 증명할 수 있음.
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// 토큰의 유효성을 확인
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey.getBytes()).build().parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
}
7. JWT 토큰 유효 검사 및 Security 등록하는 파일 추가
사용자의 요청이 들어올 경우, JWT 토큰이 유효한지 검사하고, 인증 정보를 Security로 등록하는 역할을 하는 파일입니다.
이 설정이 있어야 로그인 시 JWT 토큰을 검증하여 인증된 사용자로 처리합니다. => 이를 안 해주면 토큰이 있어도 무조건적으로 인증되지 않은 사용자로 간주하게 됩니다.
import java.io.IOException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter { // 요청마다 한 번씩만 실행되는 필터
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization"); // Android에서 보낸 Authorization 헤더를 읽음
String token = null;
String username = null;
// Authorization: Bearer "토큰" 형태의 헤더에서 실제 JWT 토큰 추출
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
if (jwtService.validateToken(token)) {
username = jwtService.extractUsername(token);
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
filterChain.doFilter(request, response);
}
}
8. 로그인 요청 및 응답 DTO 추가
LoginRequest (요청), LoginResponse (응답) DTO를 추가합니다.
요청에는 아이디, 비밀번호 값이 들어가고, 응답에는 토큰 값이 들어갑니다.
public class LoginRequest {
private String username;
private String password;
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
}
public class LoginResponse {
private String accessToken;
public LoginResponse(String accessToken) {
this.accessToken = accessToken;
}
public String getAccessToken() {
return accessToken;
}
}
9. 로그인을 처리하는 Service 추가
실제 로그인을 처리하는 로직인데, 이전에 추가했던 findByEmail을 통해 요청받은 이메일 값 (username)으로 사용자 정보가 있는지 1차적으로 조회하고, 요청받은 비밀번호 값 (password)을 암호화했을 때 조회된 사용자의 비밀번호 값과 동일한지 2차적으로 검사한 후 이메일 값 (username)을 토큰에 넣습니다.
다른 내용이긴한데, 서비스를 구성할 때 사용자의 비밀번호의 경우 무조건 암호화를 하고서 DB에 넣어야합니다. 암호화는 보통 BCryptPasswordEncoder를 사용해서 진행하는데, 이의 경우 복호화는 불가능한 알고리즘이라는 점 알고 있으시면 됩니다.
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import com.lgh.FlipMarketBackend.config.JwtService;
import com.lgh.FlipMarketBackend.entity.User;
import com.lgh.FlipMarketBackend.repository.UserRepository;
@Service
public class AuthService {
private final JwtService jwtService;
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
public AuthService(JwtService jwtService, UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
this.jwtService = jwtService;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public String login(String username, String password) {
User user = userRepository.findByEmail(username).orElseThrow(() -> new UsernameNotFoundException("사용자 없음"));
if (!passwordEncoder.matches(password, user.getPwd())) {
throw new BadCredentialsException("비밀번호 불일치");
}
return jwtService.generatedToken(username);
}
}
10. 로그인 요청 URL을 처리하는 Controller 추가
controller는 단순합니다. 일단 데이터를 처리하는 controller이기 때문에 @RestController로 선언해주고, 요청받은 이메일 값과 비밀번호 값을 통해 로그인을 실시합니다.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.lgh.FlipMarketBackend.dto.LoginRequest;
import com.lgh.FlipMarketBackend.dto.LoginResponse;
import com.lgh.FlipMarketBackend.service.AuthService;
@RestController
@RequestMapping("/api/android/")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
String token = authService.login(request.getUsername(), request.getPassword());
return ResponseEntity.ok(new LoginResponse(token));
}
}
좀 길어졌는데, 이렇게 10가지 단계를 통해 백엔드 쪽은 완성되었습니다. Spring Security + JWT 구조로 인해 많이 복잡하실텐데, 보안에 강화된 로그인, 회원가입을 구현하기 위해서는 불가피한 과정이라고 생각하시면 되겠습니다.
프론트 (Android) 설정
1. 필요한 의존성 추가
Android에서 서버와 통신할 때 자주 사용하는 Retrofit 라이브러리와 간편한 네트워크 요청을 위한 라이브러리들입니다.
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// Gson Converter (JSON 직렬화/역직렬화용)
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// OkHttp Logging (네트워크 로그 디버깅용, 선택 사항)
implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")
2. 로그인 요청 및 응답 파일 추가
이전에 boot 쪽에서 만들었던 LoginRequest, Response 파일과 거의 동일합니다. 다만, 프론트쪽의 Request 파일은 getter가 따로 필요없습니다.
package com.lgh.flipmarketandroid.dto;
public class LoginRequest {
private String username;
private String password;
public LoginRequest(String username, String password) {
this.username = username;
this.password = password;
}
}
package com.lgh.flipmarketandroid.dto;
public class LoginResponse {
private String accessToken;
public String getAccessToken() {
return accessToken;
}
}
3. Retrofit 설정
Spring Boot와 통신할 때 사용하는 파일입니다.
import android.content.Context;
import android.content.SharedPreferences;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitClient {
private static final String BASE_URL = "http://192.168.219.105:8080";
private static Retrofit retrofit = null;
public static Retrofit getInstance(final Context context) {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request original = chain.request();
SharedPreferences prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE);
String token = prefs.getString("jwt_token", null);
Request.Builder builder = original.newBuilder();
if (token != null) {
// 토큰이 존재할 경우 요청 헤더에 Authorization: Bearer "토큰" 형식으로 추가합니다.
// 이는 이전에 백엔드 쪽에서 받을 때 적었던 형식과 동일합니다.
builder.header("Authorization", "Bearer " + token);
}
Request request = builder.build();
return chain.proceed(request);
})
.build();
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()) // JSON 데이터를 자동으로 객체 변환
.build();
}
return retrofit;
}
}
4. Api 요청 파일 추가
이제 앱에서 로그인 요청 시 실제로 요청을 보낼 URL과 데이터들을 정의해주는 파일을 추가할 겁니다.
POST 어노테이션은 이전에 Spring Boot에서 설정했던 PostMapping URL을 적어주시면 되고, 파라미터로는 이메일, 비밀번호를 넘길 것이므로 이전에 추가했던 LoginRequest 파일로 인자 값을 넘깁니다.
import com.lgh.flipmarketandroid.dto.LoginRequest;
import com.lgh.flipmarketandroid.dto.LoginResponse;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;
public interface ApiService {
@POST("/api/android/login")
Call<LoginResponse> login(@Body LoginRequest request);
}
5. 로그인 화면 추가
UI는 솔직히 원하시는대로 개발하시는 것이기 때문에 아래 코드는 참고만 해주시면 될 것 같습니다. ImageView는 아직 src를 추가 안했는데, 적당한 이미지를 못 찾아서.. 추후에 추가하겠습니다.
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<!-- 앱 로고 -->
<ImageView
android:id="@+id/logoImageView"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<!-- 아이디 입력 -->
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="아이디 또는 이메일"
android:inputType="textEmailAddress"
app:layout_constraintTop_toBottomOf="@id/logoImageView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp"/>
<!-- 비밀번호 입력 -->
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="비밀번호"
android:inputType="textPassword"
app:layout_constraintTop_toBottomOf="@id/username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
<!-- 로그인 버튼 -->
<Button
android:id="@+id/loginButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="로그인"
android:backgroundTint="@color/purple_500"
android:textColor="@android:color/white"
app:layout_constraintTop_toBottomOf="@id/password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"/>
<!-- 회원가입 텍스트 -->
<TextView
android:id="@+id/signUpTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="계정이 없으신가요? 회원가입"
android:textColor="@color/purple_700"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/loginButton"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"
android:clickable="true"
android:focusable="true"/>
</androidx.constraintlayout.widget.ConstraintLayout>
6. 로그인 Activity 파일 추가
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.lgh.flipmarketandroid.R;
import com.lgh.flipmarketandroid.config.ApiService;
import com.lgh.flipmarketandroid.config.RetrofitClient;
import com.lgh.flipmarketandroid.dto.LoginRequest;
import com.lgh.flipmarketandroid.dto.LoginResponse;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
EditText usernameEditText = findViewById(R.id.username);
EditText passwordEditText = findViewById(R.id.password);
Button loginButton = findViewById(R.id.loginButton);
loginButton.setOnClickListener(e -> {
String username = usernameEditText.getText().toString();
String password = passwordEditText.getText().toString();
if (username.isEmpty() || password.isEmpty()) {
Toast.makeText(getApplicationContext(), "아이디 또는 비밀번호를 입력해주세요.", Toast.LENGTH_SHORT).show();
}
LoginRequest request = new LoginRequest(username, password);
ApiService apiService = RetrofitClient.getInstance(this).create(ApiService.class);
apiService.login(request).enqueue(new Callback<LoginResponse>() {
@Override
public void onResponse(@NonNull Call<LoginResponse> call, @NonNull Response<LoginResponse> response) {
if (response.isSuccessful() && response.body() != null) {
String token = response.body().getAccessToken();
// save
getSharedPreferences("auth", MODE_PRIVATE)
.edit().putString("jwt", token).apply();
Toast.makeText(getApplicationContext(), "로그인 성공", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "로그인 실패", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(@NonNull Call<LoginResponse> call, @NonNull Throwable t) {
Log.e("Retrofit", "Login Failed: " + t.getMessage(), t);
Toast.makeText(getApplicationContext(), "네트워크 오류", Toast.LENGTH_SHORT).show();
}
});
});
}
}
동작 확인
Spring Boot 먼저 실행해주고, Android를 실행해주면 이메일, 비밀번호, 로그인 버튼이 존재하게 됩니다.
이제 DB에 해당하는 아이디, 비밀번호 값을 가진 사용자가 존재할 경우 "로그인 성공" 알림을 띄우고, 없을 경우 "로그인 실패" 알림을 띄우게 됩니다.
네트워크 오류
저도 처음에 그랬는데, 계속해서 네트워크 오류가 발생하여서 원인을 분석해봤습니다. (네트워크 오류라고 뜨는 이유는 Toast 메시지를 네트워크 오류로 해서 그렇습니다.)
확인해보니 "uses-permission" 설정 및 "networkSecurityConfig" 설정을 해야한다고 합니다.
uses-permission 설정
<uses-permission android:name="android.permission.INTERNET" />
위의 코드를 AndroidManifest.xml 파일의 manifest 태그 사이 맨 위에 추가해줍니다. 이를 설정 안하면 통신 자체가 불가능해집니다.
networkSecurityConfig 설정
이는 따로 파일을 하나 추가했습니다. 경로는 res/xml/network_security_config.xml 파일입니다.
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
추가 완료되면 AndroidManifest.xml 파일의 application 태그의 android:networkSecurityConfig 설정을 아래와 같이 추가해줍니다. Android 9 버전 이상부터 HTTP 통신이 기본적으로 차단되기 때문에 이제는 필수적으로 허용해야하는 설정입니다.
android:networkSecurityConfig="@xml/network_security_config"
'Java > Android' 카테고리의 다른 글
[Android 공부] Spring Boot와 연계를 통한 회원가입 구현(3) (0) | 2025.06.02 |
---|---|
[Android 공부] Spring Boot와 연계를 통한 회원가입 구현 - 이메일 중복 체크(2) (0) | 2025.06.02 |