웹이든 앱이든 자유롭게 주제를 선정하여 기능 구현에 초점을 맞춰 결과물을 제출하라는 팀프로젝트 과제를 수행하고 있다. 안드로이드에 연동할 Rest API 서버를 만들고자 하는데 안드로이드는 처음이라 클라이언트와 서버를 우선 연결하는 데에 초점을 두었다.
Android Studio와 IntelliJ에서 각각 코드를 실행하면 내 로컬 PC에선 다음과 같은 구조로 실행되고 있고 앱과 서버를 연동하고 싶다.
이 때 내가 착각했던 것은 Android에서 localhost를 연결하면 localhost:8080에서 돌고있는 Spring Boot와 바로 연결이 될 줄 알았지만 결과적으로는 통신할 수 없다.
그 이유는 다음과 같다.
Android에서 localhost를 호출하는 것은 안드로이드 디바이스(에뮬레이터) 내의 localhost로 연결하라는 소리와 같다.
에뮬레이터의 인스턴스는 개발 머신(나의 컴퓨터)과 격리되어, 가상 라우터와 방화벽 서비스 뒤에서 실행된다고 한다. 따라서 개발 머신의 127.0.0.1은 애뮬레이터 네트워크 상에서 10.0.2.2로 사용해야 한다.
즉, 개발 머신의 루프백 인터페이스(개발 머신의 127.0.0.1)에서 애뮬레이터의 서비스에 액세스 하기 위해서는 특수한 주소를 사용해야 한다.
자세한 내용은 Android Emulator 네트워킹 설정을 참고하자.
또한 이와 별개로 설정상에서 빼먹은 부분도 있었다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.registerloginexample">
<!--인터넷 권한 선언-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RegisterLoginExample">
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".RegisterActivity"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true">
</activity>
</application>
</manifest>
인터넷 권한 선언을 하는 부분만 넣어주면 됐겠지 싶었는데 아무런 요청/응답이 없었는데 <application>
내에 android:usesCleartextTraffic="true"
이것을 빼먹었었다.
예제 코드를 짜보면서 연결은 성공했으나 Controller에서 application/x-www-form-urlencoded를 인식할 수 없다는 Error를 뿜어대고 있다.
여기저기 구글링을 해서 찾은 결과는 다음과 같았다.
Spring Boot 상의 코드가 문제일까 싶어 Controller의 코드만 신경써서 위 방법들로 어찌저찌 해결했었다.
어떻게 때려맞춰 돌아가긴 하지만 아무리봐도 이건 아닌거 같아 좀 더 고민해보다 주니어 백엔드 개발자이신 학교 선배님한테 여쭤봤더니 위 코드도 원래대로 다시 수정하고 안드로이드 상의 Request 코드를 수정해야 할 것 같다고 하셨다.
처음 application/x-www-form-urlencoded 문제가 발생했을 때 짰던 코드는 사실 문제가 없었던 것이었다. 내가 알고있던 내용 처럼 Spring Boot의 @RequestBody
애노테이션이 Json을 자동 파싱해서 파라미터로 들어가 있는 Dto 객체에 해당하는 값들을 넣어 처리하는 구조가 맞다.
따라서 다음과 같이 원래대로 수정하고 Android의 코드를 수정해야 했다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/register")
@Slf4j
public class RegisterController {
private final MemberService memberService;
@PostMapping("/test")
public String postMember(@RequestBody MemberDto memberDto) {
// log.info(); memberDto로 넘어갈 파라미터들이 잘 파싱 됐는지 확인
memberService.join(memberDto);
return "ok";
}
}
다음과 같이 입력하여 서버로 넘기고자 하는데, 각각 userId, userPass, userName, userAge로 넘어간다. 기본키인 id는 파라미터로 넘어가지 않기 때문에 컨트롤러에서 null값을 받을 수 없다는 오류가 발생했고, id값은 auto increments를 적용하고 싶은데 무엇이 문제일까 싶었다. 이 것은 Entity와 Dto 클래스를 수정하면 해결할 수 있는 문제였다.
package com.example.SpringBootForAndroid.androidTest.domain.entity;
import com.example.SpringBootForAndroid.androidTest.domain.dto.MemberDto;
import lombok.*;
import javax.persistence.*;
@Entity
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String userId;
@Column(nullable = false)
private String userPass;
@Column(nullable = false)
private String userName;
@Column(nullable = false)
private Integer userAge;
@Builder
public Member(String userId, String userPass, String userName, Integer userAge) {
this.userId = userId;
this.userPass = userPass;
this.userName = userName;
this.userAge = userAge;
}
public MemberDto toDto() {
return MemberDto.builder()
.userId(userId)
.userPass(userPass)
.userName(userName)
.userAge(userAge)
.build();
}
}
@Builder
를 통해 id는 넘기지 않고 나머지 속성들만 처리할 수 있도록 코드를 수정하였다.
package com.example.SpringBootForAndroid.androidTest.domain.dto;
import com.example.SpringBootForAndroid.androidTest.domain.entity.Member;
import lombok.*;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Getter
public class MemberDto {
private String userId;
private String userPass;
private String userName;
private Integer userAge;
public Member toEntity() {
return Member.builder()
.userId(userId)
.userPass(userPass)
.userName(userName)
.userAge(userAge)
.build();
}
}
마찬가지로 id값을 건들지 않도록 수정한 결과 Controller에서 id를 받지 못했다는 문제는 해결되어 다음과 같이 DB에 원하는 결과가 나왔다.
어쨌든 모든 것은 Controller에 대한 이해도가 부족해서 겪었던 문제들이었다.
JPA를 공부할 때 Maven으로 Build를 설정해서 프로젝트를 생성하다보니 Gradle 환경에서 H2 Database를 써보지 않았었다. start.spring.io에서 dependency 설정창에 h2를 고르면 자동으로 설정이 되긴 하지만, 항상 H2를 실행할 때 연결이 안되고 헷갈렸던 걸 그냥 적어놓고 가련다.
셋팅은 다음과 같이 해두자.
plugins {
id 'org.springframework.boot' version '2.6.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
server:
port: 8080
spring:
h2:
console:
enabled: true
path: /test_db
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:testdb
username: sa
password:
Spring Boot를 실행한 뒤 localhost:8080/test_db로 가면 다음과 같이 됨.
connect 누르면 연결 완료.