
Spring Boot의 Controller에서 요청 데이터를 받고 Service와 MyBatis Mapper를 연결해 SQL을 실행한 뒤 JUnit으로 검증하고 Swagger로 API를 명세하는 방법을 학습했다.
@Component 계열 어노테이션@Controller와 @RestController@RequestParam, @PathVariable, DTOparameterType, resultType, #{} 문법@Tag, @Operation, @Parameter, @Schema, @ApiResponse일반 Java에서는 개발자가 new를 사용해 객체를 직접 생성한다.
UserService userService = new UserPlainServiceImpl();
Spring에서는 객체 생성을 Spring 컨테이너에 위임할 수 있다.
@Service
public class UserPlainServiceImpl implements UserService {
}
Spring이 생성하고 관리하는 객체를 Spring Bean이라고 한다.
Spring은 애플리케이션 실행 시 Component Scan을 수행하여 지정된 패키지에서 관련 어노테이션이 붙은 클래스를 찾고 객체로 생성한다.
| 어노테이션 | 적용 계층 | 역할 |
|---|---|---|
@Component | 공통 | 일반적인 Spring Bean 등록 |
@Controller | Controller | 화면을 반환하는 MVC Controller |
@RestController | Controller | 데이터를 반환하는 REST Controller |
@Service | Service | 비즈니스 로직 처리 |
@Repository | Repository | 데이터 접근 및 예외 변환 |
@Mapper | MyBatis Mapper | Mapper 프록시 객체 등록 |
@Controller, @RestController, @Service, @Repository는 @Component를 기반으로 한다.
MyBatis의 @Mapper는 @Component의 하위 어노테이션은 아니지만, MyBatis가 Mapper 인터페이스의 프록시 객체를 만들고 Spring Bean으로 등록하도록 표시한다.
@Controller와 @RestController@Controller요청을 처리한 뒤 기본적으로 View의 이름을 반환한다.
@Controller
public class PageController {
@GetMapping("/main")
public String main() {
return "main";
}
}
반환된 "main"은 문자열 데이터가 아니라 일반적으로 main.html이나 main.jsp와 같은 화면을 의미한다.
데이터를 응답 본문에 직접 반환하려면 @ResponseBody가 필요하다.
@Controller
public class PageController {
@ResponseBody
@GetMapping("/message")
public String message() {
return "Hello Spring";
}
}
@RestControllerREST API를 구현할 때 사용하며, 메서드의 반환값을 HTTP 응답 본문에 직접 담는다.
@RestController
public class UserController {
@GetMapping("/user")
public UserResponseDTO user() {
return UserResponseDTO.builder()
.email("user@example.com")
.name("사용자")
.build();
}
}
객체를 반환하면 Spring의 메시지 변환기가 일반적으로 JSON으로 변환한다.
{
"email": "user@example.com",
"name": "사용자"
}
다음 두 선언은 비슷한 역할을 한다.
@RestController
@Controller
@ResponseBody
Endpoint는 클라이언트가 서버의 기능을 호출하기 위해 접근하는 API의 접점이다.
사용자 요청
→ Endpoint
→ Controller
→ Action Method
실습에서는 Health Check API를 구현했다.
@RestController
@RequestMapping("/health")
public class HealthController {
@GetMapping("/alive")
public String getMethodName() {
return "alive";
}
}
클래스의 @RequestMapping 경로와 메서드의 @GetMapping 경로가 결합되어 최종 Endpoint가 만들어진다.
GET http://localhost:8000/health/alive
응답은 다음과 같다.
alive
HTTP Method별 Mapping 어노테이션은 다음과 같다.
| 어노테이션 | HTTP Method | 일반적인 역할 |
|---|---|---|
@GetMapping | GET | 데이터 조회 |
@PostMapping | POST | 데이터 등록 |
@PutMapping | PUT | 전체 데이터 수정 |
@PatchMapping | PATCH | 일부 데이터 수정 |
@DeleteMapping | DELETE | 데이터 삭제 |
REST API에서는 URL은 자원을 표현하고 HTTP Method가 수행할 행동을 표현한다.
GET /users/1 → 사용자 조회
POST /users → 사용자 등록
PUT /users/1 → 사용자 전체 수정
DELETE /users/1 → 사용자 삭제
@RequestParamURL의 Query String으로 전달된 값을 개별 변수로 받는다.
@GetMapping("/signIn")
public ResponseEntity<?> signIn(
@RequestParam String email,
@RequestParam String password) {
return ResponseEntity.ok().build();
}
요청 예시는 다음과 같다.
GET /users/signIn?email=user@example.com&password=1234
@PathVariableURL 경로에 포함된 값을 받는다.
@GetMapping("/{userId}")
public ResponseEntity<?> findUser(
@PathVariable Long userId) {
return ResponseEntity.ok(userId);
}
요청 예시는 다음과 같다.
GET /users/1
이때 1이 userId에 바인딩된다.
로그인 요청에서는 여러 파라미터를 UserRequestDTO로 한 번에 받았다.
@GetMapping("/signIn")
public ResponseEntity<?> signIn(UserRequestDTO request) {
System.out.println(
"debug >>>> user controller signIn params " + request
);
return null;
}
다음 요청의 Query Parameter가 DTO의 같은 이름을 가진 필드에 자동으로 바인딩된다.
GET /users/signIn?email=user@example.com&password=1234
email → request.email
password → request.password
Query Parameter를 DTO로 받는다는 사실을 명확하게 나타내려면 @ModelAttribute를 사용할 수 있다.
@GetMapping("/signIn")
public ResponseEntity<?> signIn(
@ModelAttribute UserRequestDTO request) {
return ResponseEntity.ok().build();
}
UserRequestDTO클라이언트가 서버로 전달한 데이터를 받는 객체이다.
@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserRequestDTO {
private String email;
private String password;
private String name;
}
| Lombok 어노테이션 | 역할 |
|---|---|
@Builder | Builder 방식으로 객체 생성 |
@Getter | Getter 메서드 생성 |
@Setter | Setter 메서드 생성 |
@ToString | 객체 내용을 문자열로 출력 |
@NoArgsConstructor | 기본 생성자 생성 |
@AllArgsConstructor | 모든 필드를 받는 생성자 생성 |
UserResponseDTO서버가 클라이언트에 반환할 데이터를 표현하는 객체이다.
@Builder
@Getter
@ToString
public class UserResponseDTO {
private String email;
private String password;
private String name;
}
요청 DTO와 응답 DTO를 분리하면 입력 데이터와 출력 데이터를 서로 다르게 관리할 수 있다.
실제 서비스에서는 보안을 위해 비밀번호를 응답 DTO에 포함하지 않는 것이 좋다.
@Builder
@Getter
@ToString
public class UserResponseDTO {
private String email;
private String name;
}
비밀번호를 toString()이나 로그를 통해 출력하지 않도록 주의해야 한다.
Service 인터페이스는 Controller가 사용할 기능의 규격을 정의한다.
public interface UserService {
UserResponseDTO signIn(UserRequestDTO request);
}
실습에서는 동일한 인터페이스를 구현하는 Service 구현체를 두 개 만들었다.
@Service("encryption")
public class UserEncrytionServiceImpl implements UserService {
@Override
public UserResponseDTO signIn(
UserRequestDTO request) {
System.out.println(
"debug >>>> user encryption service signIn"
);
return null;
}
}
@Service("plain")
public class UserPlainServiceImpl implements UserService {
@Override
public UserResponseDTO signIn(
UserRequestDTO request) {
System.out.println(
"debug >>>> user plain service signIn"
);
return null;
}
}
두 구현체가 모두 UserService 타입이기 때문에 타입만으로는 어떤 객체를 주입할지 결정할 수 없다.
실습에서는 @Resource를 사용하여 Bean 이름으로 주입 대상을 선택했다.
@Resource(name = "encryption")
private UserService userService;
따라서 이름이 encryption인 UserEncrytionServiceImpl 객체가 주입된다.
클래스 이름의 Encrytion은 오타이므로 다음과 같이 수정하는 것이 좋다.
UserEncrytionServiceImpl
↓
UserEncryptionServiceImpl
MyBatis는 Java 메서드와 SQL을 연결해 주는 SQL Mapper Framework이다.
JDBC에서는 개발자가 다음과 같은 과정을 직접 처리해야 했다.
Connection 생성
→ PreparedStatement 생성
→ SQL 파라미터 설정
→ SQL 실행
→ ResultSet 처리
→ DTO 생성
→ 자원 종료
MyBatis는 이러한 반복 작업을 줄여준다.
다만 JPA와 달리 실제 SQL은 개발자가 직접 작성해야 한다.
Mapper 인터페이스
↕
Mapper XML
↕
MariaDB
@Mapper
public interface UserMapper {
int save(UserRequestDTO request);
List<UserResponseDTO> findByAll();
Optional<UserResponseDTO> login(
UserRequestDTO request
);
}
@Mapper가 붙은 인터페이스는 별도의 구현 클래스를 작성하지 않는다.
MyBatis가 실행 시점에 Mapper 인터페이스의 프록시 구현 객체를 생성하여 Spring Bean으로 등록한다.
따라서 다음과 같이 의존성을 주입받을 수 있다.
@Autowired
private UserMapper userMapper;
| 메서드 | 역할 | 반환값 |
|---|---|---|
save() | 사용자 등록 | 반영된 행의 수 |
findByAll() | 전체 사용자 조회 | 사용자 목록 |
login() | 로그인 정보 조회 | 조회 결과 또는 빈 값 |
login()은 조건에 맞는 사용자가 없을 수 있으므로 Optional을 사용했다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace=
"com.example.testcase.features.users.repository.UserMapper">
<insert id="save"
parameterType=
"com.example.testcase.features.users.domain.dto.UserRequestDTO">
INSERT INTO SPRING_USER_TBL(
EMAIL,
PASSWORD,
NAME
)
VALUES(
#{email},
#{password},
#{name}
)
</insert>
<select id="findByAll"
resultType=
"com.example.testcase.features.users.domain.dto.UserResponseDTO">
SELECT
EMAIL,
PASSWORD,
NAME
FROM
SPRING_USER_TBL
</select>
<select id="login"
parameterType=
"com.example.testcase.features.users.domain.dto.UserRequestDTO"
resultType=
"com.example.testcase.features.users.domain.dto.UserResponseDTO">
SELECT
EMAIL,
PASSWORD,
NAME
FROM
SPRING_USER_TBL
WHERE
EMAIL = #{email}
AND PASSWORD = #{password}
</select>
</mapper>
MyBatis가 Mapper 인터페이스와 Mapper XML을 연결하려면 두 가지 규칙을 지켜야 한다.
namespace와 인터페이스의 전체 경로가 같아야 한다<mapper namespace=
"com.example.testcase.features.users.repository.UserMapper">
id와 인터페이스의 메서드 이름이 같아야 한다| Mapper 메서드 | Mapper XML |
|---|---|
save() | <insert id="save"> |
findByAll() | <select id="findByAll"> |
login() | <select id="login"> |
다음 메서드를 호출하면,
int flag = userMapper.save(request);
MyBatis는 Mapper XML에서 다음 SQL을 찾아 실행한다.
<insert id="save">
parameterType, resultType, #{}parameterTypeSQL에 전달되는 입력 객체의 타입이다.
parameterType=
"com.example.testcase.features.users.domain.dto.UserRequestDTO"
#{}전달받은 객체의 프로퍼티 값을 SQL 파라미터로 사용한다.
VALUES(
#{email},
#{password},
#{name}
)
다음 DTO가 전달되었다면,
UserRequestDTO request =
UserRequestDTO.builder()
.email("jslim9413@gmail.com")
.password("1234")
.name("임정섭")
.build();
MyBatis는 다음과 같이 값을 연결한다.
#{email} → request.getEmail()
#{password} → request.getPassword()
#{name} → request.getName()
#{}는 PreparedStatement의 바인딩 파라미터로 처리되므로 문자열을 직접 연결하는 방식보다 SQL Injection 방지에 유리하다.
resultType조회된 한 행을 어떤 객체로 변환할지 지정한다.
resultType=
"com.example.testcase.features.users.domain.dto.UserResponseDTO"
DB 컬럼과 DTO 프로퍼티가 연결된다.
EMAIL → email
PASSWORD → password
NAME → name
여러 행이 조회되면 각 행을 UserResponseDTO로 변환한 뒤 List로 반환한다.
List<UserResponseDTO> findByAll();
Spring Profile을 이용하면 개발 환경과 운영 환경의 설정을 분리할 수 있다.
application.yml기본으로 사용할 Profile을 지정한다.
spring:
profiles:
default: dev
application-dev.ymlserver:
port: 8000
spring:
config:
activate:
on-profile: dev
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:3306/lgcns
username: root
password: 123456789
mybatis:
mapper-locations: classpath:/mappers/**/*Mapper.xml
| 설정 | 의미 |
|---|---|
server.port | 서버 실행 포트 |
spring.datasource.driver-class-name | MariaDB JDBC Driver |
spring.datasource.url | DB 접속 주소 |
spring.datasource.username | DB 사용자 |
spring.datasource.password | DB 비밀번호 |
mybatis.mapper-locations | Mapper XML 탐색 경로 |
Mapper XML은 설정한 경로와 파일 이름 패턴에 맞게 배치해야 한다.
src/main/resources/
├── application.yml
├── application-dev.yml
├── application-prod.yml
└── mappers/
└── users/
└── userMapper.xml
작성한 운영 설정 파일 이름이 application-prod.dev라면 Spring 설정 파일로 정상 인식되지 않는다.
다음과 같이 수정해야 한다.
application-prod.yml
spring:
config:
activate:
on-profile: prod
실제 프로젝트에서는 DB 비밀번호를 Git 저장소에 직접 올리지 않고 환경변수나 별도의 비공개 설정으로 관리해야 한다.
@SpringBootTest를 사용하면 Spring Application Context를 실행한 상태에서 Spring Bean과 DB 연결을 테스트할 수 있다.
@SpringBootTest
public class UserApplicationTests {
@Autowired
private UserMapper userMapper;
}
테스트 코드는 다음 구조로 작성했다.
Given: 테스트에 필요한 데이터 준비
When: 테스트할 기능 실행
Then: 실행 결과 검증
@Test
public void signUp() {
// given
UserRequestDTO request =
UserRequestDTO.builder()
.email("jslim9413@gmail.com")
.password("1234")
.name("임정섭")
.build();
// when
int flag = userMapper.save(request);
// then
Assertions.assertEquals(1, flag);
}
INSERT가 정상적으로 실행되어 한 행이 추가되면 1이 반환된다.
Assertions.assertEquals(1, flag);
동일한 이메일에 UNIQUE 제약조건이 있다면 테스트를 반복 실행할 때 중복 데이터 오류가 발생할 수 있다는 점도 주의해야 한다.
@Test
public void list() {
// when
List<UserResponseDTO> list =
userMapper.findByAll();
// then
list.forEach(System.out::println);
Assertions.assertNotNull(list);
Assertions.assertFalse(list.isEmpty());
}
단순히 결과를 출력하는 것만으로는 테스트 성공 여부를 자동으로 판단하기 어렵다.
따라서 Assertions를 이용해 실제 결과를 검증하는 것이 좋다.
처음 작성한 테스트에서는 로그인 요청 이메일과 예상 이메일이 달랐다.
// 요청 이메일
.email("jslim@gmail.com")
// 예상 이메일
Assertions.assertEquals(
"jslim9413@gmail.com",
response.get().getEmail()
);
로그인 SQL은 이메일과 비밀번호가 모두 일치하는 데이터를 조회한다.
WHERE EMAIL = #{email}
AND PASSWORD = #{password}
DB에 jslim@gmail.com이 없다면 Optional.empty()가 반환된다.
이 상태에서 다음 코드를 실행하면 NoSuchElementException이 발생할 수 있다.
response.get()
입력값과 예상값을 통일하고, 조회 결과의 존재 여부를 먼저 검사하도록 수정할 수 있다.
@Test
public void signIn() {
// given
UserRequestDTO request =
UserRequestDTO.builder()
.email("jslim9413@gmail.com")
.password("1234")
.build();
// when
Optional<UserResponseDTO> response =
userMapper.login(request);
// then
Assertions.assertTrue(response.isPresent());
Assertions.assertEquals(
"jslim9413@gmail.com",
response.get().getEmail()
);
}
테스트 메서드 이름에도 오타가 있었다.
singIn() → signIn()
Optional의 값을 바로 꺼내기보다 의미 있는 예외를 지정할 수도 있다.
UserResponseDTO user = response.orElseThrow(
() -> new RuntimeException(
"이메일 또는 비밀번호가 일치하지 않습니다."
)
);
Swagger는 REST API의 정보를 문서화하고 브라우저에서 직접 요청을 테스트할 수 있도록 지원한다.
주요 문서화 대상은 다음과 같다.
Spring Boot에서는 일반적으로 OpenAPI 명세와 Springdoc을 이용한다.
Gradle에는 Spring Boot 버전과 호환되는 의존성을 추가해야 한다.
dependencies {
implementation(
'org.springdoc:springdoc-openapi-starter-webmvc-ui:호환버전'
)
}
Swagger UI의 기본 접속 주소는 일반적으로 다음과 같다.
http://localhost:8000/swagger-ui/index.html
OpenAPI JSON 명세는 다음 주소에서 확인할 수 있다.
http://localhost:8000/v3/api-docs
| 어노테이션 | 적용 위치 | 역할 |
|---|---|---|
@Tag | Controller | API 그룹 이름과 설명 |
@Operation | Controller 메서드 | API 기능 설명 |
@Parameter | 메서드 파라미터 | 개별 요청값 설명 |
@ParameterObject | DTO 파라미터 | DTO를 Query Parameter로 표현 |
@Schema | DTO 및 필드 | 데이터 구조 설명 |
@ApiResponse | Controller 메서드 | 응답 상태와 내용 설명 |
@ApiResponses | Controller 메서드 | 여러 응답 상태를 묶어서 명세 |
@TagController가 담당하는 API를 하나의 그룹으로 묶는다.
@Tag(
name = "User API",
description = "회원 가입, 로그인 및 회원 조회 API"
)
@RestController
@RequestMapping("/users")
public class UserController {
}
@Operation각 API가 수행하는 기능을 설명한다.
@Operation(
summary = "사용자 로그인",
description = "이메일과 비밀번호를 검증합니다."
)
@GetMapping("/signIn")
public ResponseEntity<?> signIn(
UserRequestDTO request) {
return null;
}
@Parameter개별 파라미터를 설명한다.
@GetMapping("/signIn")
public ResponseEntity<?> signIn(
@Parameter(
description = "사용자 이메일",
required = true,
example = "user@example.com"
)
@RequestParam String email,
@Parameter(
description = "사용자 비밀번호",
required = true,
example = "1234"
)
@RequestParam String password
) {
return ResponseEntity.ok().build();
}
@SchemaDTO 또는 DTO 필드의 구조와 예시를 정의한다.
@Schema(description = "사용자 요청 DTO")
public class UserRequestDTO {
@Schema(
description = "사용자 이메일",
example = "user@example.com",
requiredMode = Schema.RequiredMode.REQUIRED
)
private String email;
@Schema(
description = "사용자 비밀번호",
example = "1234",
requiredMode = Schema.RequiredMode.REQUIRED
)
private String password;
@Schema(
description = "사용자 이름",
example = "임정섭"
)
private String name;
}
DTO를 Query Parameter 목록으로 문서화하려면 @ParameterObject를 사용할 수 있다.
@GetMapping("/signIn")
public ResponseEntity<?> signIn(
@ParameterObject UserRequestDTO request) {
return ResponseEntity.ok().build();
}
@ApiResponse를 이용한 응답 명세@Operation(
summary = "사용자 로그인",
description = "이메일과 비밀번호를 검증하고 사용자 정보를 반환합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그인 성공"
),
@ApiResponse(
responseCode = "400",
description = "필수 파라미터 누락"
),
@ApiResponse(
responseCode = "401",
description = "이메일 또는 비밀번호 불일치"
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류"
)
})
@GetMapping("/signIn")
public ResponseEntity<?> signIn(
@ParameterObject UserRequestDTO request) {
UserResponseDTO response =
userService.signIn(request);
return ResponseEntity.ok(response);
}
성공 응답의 DTO 구조까지 명시할 수 있다.
@ApiResponse(
responseCode = "200",
description = "로그인 성공",
content = @Content(
mediaType = "application/json",
schema = @Schema(
implementation = UserResponseDTO.class
)
)
)
Swagger 어노테이션은 실제 API 동작을 구현하는 것이 아니라 API의 사용 방법을 설명하는 문서 명세 역할을 한다.
예를 들어 문서에 401 응답을 작성해도 실제 API가 자동으로 401을 반환하지는 않는다.
실제 코드에서도 상태 코드를 구현해야 한다.
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("이메일 또는 비밀번호가 일치하지 않습니다.");
클라이언트
↓
GET /users/signIn?email=...&password=...
↓
UserController
↓
UserRequestDTO에 파라미터 바인딩
↓
UserService.signIn()
↓
UserMapper.login()
↓
userMapper.xml의 login SQL
↓
MariaDB SPRING_USER_TBL 조회
↓
UserResponseDTO로 결과 매핑
↓
ResponseEntity로 HTTP 응답
Swagger는 이 API의 Endpoint, 요청값, 응답값, 상태 코드를 문서로 표현한다.
JUnit은 Controller를 거치지 않고 Mapper를 직접 호출하여 MyBatis와 DB 연결 및 SQL 실행 결과를 검증했다.
@RestController를 사용하면 Java 객체를 JSON 형태로 응답할 수 있다.@RequestParam으로 각각 받거나 DTO로 묶어 받을 수 있다.namespace와 SQL id를 기준으로 연결된다.parameterType은 SQL의 입력 타입이고 resultType은 조회 결과가 변환될 타입이다.#{}는 DTO 프로퍼티 값을 PreparedStatement 파라미터로 바인딩한다.@Mapper도 @Component의 하위 어노테이션인가?@Controller, @RestController, @Service, @Repository는 @Component를 기반으로 한다.
MyBatis의 @Mapper는 @Component의 하위 어노테이션은 아니며, MyBatis가 Mapper 인터페이스를 인식하고 프록시 객체를 Bean으로 등록하게 한다.
@Controller와 @RestController의 차이는 무엇인가?@Controller: 기본적으로 View 반환@RestController: 기본적으로 HTTP 응답 본문에 데이터 반환@RequestParam 없이 DTO로 요청을 받을 수 있는가?GET 요청의 Query Parameter와 DTO의 필드 이름이 같다면 Spring이 DTO에 자동으로 값을 바인딩할 수 있다.
?email=user@example.com&password=1234
public ResponseEntity<?> signIn(
UserRequestDTO request)
아니다. @ApiResponse는 문서 명세만 담당한다.
실제 상태 코드는 ResponseEntity 또는 예외 처리 코드로 별도로 구현해야 한다.
Optional.get()을 바로 사용해도 되는가?조회 결과가 없으면 Optional.empty()가 반환되므로 바로 get()을 호출하면 예외가 발생할 수 있다.
먼저 isPresent()로 확인하거나 orElseThrow()를 사용해야 한다.
/health/alive Health Check API 구현UserRequestDTO, UserResponseDTO 작성UserService 인터페이스 작성@Resource(name = "encryption")으로 특정 구현체 주입UserMapper 인터페이스 작성userMapper.xml에 INSERT와 SELECT SQL 작성SPRING_USER_TBL과 MyBatis 연결application-dev.yml 작성8000 포트에서 실행하도록 설정했다./health/alive 요청으로 서버 동작 여부를 확인할 수 있게 되었다.로그인 요청과 예상 결과에서 서로 다른 이메일을 사용했다.
.email("jslim@gmail.com")
Assertions.assertEquals(
"jslim9413@gmail.com",
response.get().getEmail()
);
입력 이메일과 예상 이메일을 같은 값으로 수정했다.
.email("jslim9413@gmail.com")
조회 결과가 존재하는지도 먼저 검사했다.
Assertions.assertTrue(response.isPresent());
Optional.get() 사용 시 예외 가능성로그인에 실패하면 Optional.empty()가 반환되기 때문에 get()을 호출할 때 예외가 발생할 수 있다.
먼저 존재 여부를 검증하거나 orElseThrow()를 사용한다.
UserResponseDTO user = response.orElseThrow(
() -> new RuntimeException(
"이메일 또는 비밀번호가 일치하지 않습니다."
)
);
운영 설정 파일을 다음과 같이 작성했다.
application-prod.dev
Spring Boot가 인식할 수 있는 YAML 파일로 수정한다.
application-prod.yml
UserService를 구현한 클래스가 두 개이므로 타입만으로는 주입 대상을 결정하기 어렵다.
각 Service에 Bean 이름을 지정하고 @Resource로 주입할 Bean을 선택했다.
@Service("encryption")
@Resource(name = "encryption")
private UserService userService;
@ApiResponse(responseCode = "401")을 작성하면 실제 API도 자동으로 401을 반환한다고 혼동할 수 있다.
Swagger 어노테이션은 명세만 담당하며 실제 동작은 별도로 구현해야 한다.
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("이메일 또는 비밀번호가 일치하지 않습니다.");
UserService에서 UserMapper.login()을 실제로 호출하도록 구현하기ResponseEntity 반환하기UserResponseDTO에서 비밀번호 필드 제거하기@PostMapping, @RequestBody를 이용한 JSON 요청 처리 학습하기resultMap, 동적 SQL, Type Alias 학습하기오늘은 Spring Boot가 사용자의 HTTP 요청을 받아 처리하는 Controller의 기본 구조부터 Service와 MyBatis를 거쳐 MariaDB에 접근하는 전체 흐름을 학습했다.
이전 Java 과정에서 직접 만들었던 Controller, Service, DAO 구조가 Spring에서는 어노테이션과 의존성 주입을 통해 더욱 체계적으로 연결된다는 것을 확인했다.
특히 MyBatis에서는 Mapper 인터페이스와 Mapper XML이 분리되어 있지만, namespace와 SQL id를 통해 하나의 기능으로 연결된다는 점이 핵심이었다.
JUnit을 이용하여 Mapper와 SQL의 실행 결과를 검증했으며, Swagger를 통해 구현한 API의 Endpoint, 요청 데이터, 응답 데이터 및 상태 코드를 문서로 명세하는 방법도 배웠다.
오늘 학습의 가장 중요한 흐름은 다음과 같다.
Controller
→ Service
→ Mapper Interface
→ Mapper XML
→ Database
앞으로는 현재 null을 반환하는 Service와 Controller를 실제 MyBatis 결과와 연결하고, 로그인 실패와 서버 오류에 대한 예외 처리 및 적절한 HTTP 응답까지 구현해야 한다.