[LG CNS AM INSPIRE 6기] 25일차 TIL: Spring Boot Controller, MyBatis와 Swagger API 문서화

펭귄's 다이어리·2026년 9월 3일
post-thumbnail

오늘의 한 줄 요약

Spring Boot의 Controller에서 요청 데이터를 받고 Service와 MyBatis Mapper를 연결해 SQL을 실행한 뒤 JUnit으로 검증하고 Swagger로 API를 명세하는 방법을 학습했다.


배운 내용

  • Spring Bean과 객체 생성 위임
  • @Component 계열 어노테이션
  • @Controller와 @RestController
  • Endpoint와 Mapping
  • @RequestParam, @PathVariable, DTO
  • Service 인터페이스와 구현체
  • 이름을 이용한 의존성 주입
  • MyBatis Mapper 인터페이스
  • Mapper XML과 SQL 작성
  • parameterType, resultType, #{} 문법
  • Spring Profile과 환경별 설정 파일
  • JUnit과 Given–When–Then 테스트
  • Swagger 기반 API 명세화
  • @Tag, @Operation, @Parameter, @Schema, @ApiResponse

핵심 개념

1. Spring Bean과 객체 생성 위임

일반 Java에서는 개발자가 new를 사용해 객체를 직접 생성한다.

UserService userService = new UserPlainServiceImpl();

Spring에서는 객체 생성을 Spring 컨테이너에 위임할 수 있다.

@Service
public class UserPlainServiceImpl implements UserService {
}

Spring이 생성하고 관리하는 객체를 Spring Bean이라고 한다.

Spring은 애플리케이션 실행 시 Component Scan을 수행하여 지정된 패키지에서 관련 어노테이션이 붙은 클래스를 찾고 객체로 생성한다.

어노테이션적용 계층역할
@Component공통일반적인 Spring Bean 등록
@ControllerController화면을 반환하는 MVC Controller
@RestControllerController데이터를 반환하는 REST Controller
@ServiceService비즈니스 로직 처리
@RepositoryRepository데이터 접근 및 예외 변환
@MapperMyBatis MapperMapper 프록시 객체 등록

@Controller, @RestController, @Service, @Repository는 @Component를 기반으로 한다.

MyBatis의 @Mapper는 @Component의 하위 어노테이션은 아니지만, MyBatis가 Mapper 인터페이스의 프록시 객체를 만들고 Spring Bean으로 등록하도록 표시한다.


2. @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";
    }
}

@RestController

REST 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

3. Endpoint와 Mapping

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일반적인 역할
@GetMappingGET데이터 조회
@PostMappingPOST데이터 등록
@PutMappingPUT전체 데이터 수정
@PatchMappingPATCH일부 데이터 수정
@DeleteMappingDELETE데이터 삭제

REST API에서는 URL은 자원을 표현하고 HTTP Method가 수행할 행동을 표현한다.

GET    /users/1 → 사용자 조회
POST   /users   → 사용자 등록
PUT    /users/1 → 사용자 전체 수정
DELETE /users/1 → 사용자 삭제

4. Controller에서 요청 파라미터 받기

@RequestParam

URL의 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

@PathVariable

URL 경로에 포함된 값을 받는다.

@GetMapping("/{userId}")
public ResponseEntity<?> findUser(
        @PathVariable Long userId) {

    return ResponseEntity.ok(userId);
}

요청 예시는 다음과 같다.

GET /users/1

이때 1이 userId에 바인딩된다.

DTO로 여러 파라미터 받기

로그인 요청에서는 여러 파라미터를 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();
}

5. 요청 DTO와 응답 DTO

UserRequestDTO

클라이언트가 서버로 전달한 데이터를 받는 객체이다.

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserRequestDTO {

    private String email;
    private String password;
    private String name;
}
Lombok 어노테이션역할
@BuilderBuilder 방식으로 객체 생성
@GetterGetter 메서드 생성
@SetterSetter 메서드 생성
@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()이나 로그를 통해 출력하지 않도록 주의해야 한다.


6. Service 인터페이스와 구현체

Service 인터페이스는 Controller가 사용할 기능의 규격을 정의한다.

public interface UserService {

    UserResponseDTO signIn(UserRequestDTO request);
}

실습에서는 동일한 인터페이스를 구현하는 Service 구현체를 두 개 만들었다.

암호화 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

@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

7. MyBatis의 역할

MyBatis는 Java 메서드와 SQL을 연결해 주는 SQL Mapper Framework이다.

JDBC에서는 개발자가 다음과 같은 과정을 직접 처리해야 했다.

Connection 생성
→ PreparedStatement 생성
→ SQL 파라미터 설정
→ SQL 실행
→ ResultSet 처리
→ DTO 생성
→ 자원 종료

MyBatis는 이러한 반복 작업을 줄여준다.

다만 JPA와 달리 실제 SQL은 개발자가 직접 작성해야 한다.

Mapper 인터페이스
    ↕
Mapper XML
    ↕
MariaDB

8. Mapper 인터페이스

@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을 사용했다.


9. Mapper XML 작성

<?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>

10. Mapper 인터페이스와 XML의 연결 규칙

MyBatis가 Mapper 인터페이스와 Mapper XML을 연결하려면 두 가지 규칙을 지켜야 한다.

첫 번째: namespace와 인터페이스의 전체 경로가 같아야 한다

<mapper namespace=
    "com.example.testcase.features.users.repository.UserMapper">

두 번째: SQL의 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">

11. parameterType, resultType, #{}

parameterType

SQL에 전달되는 입력 객체의 타입이다.

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();

12. Spring Profile과 환경별 설정

Spring Profile을 이용하면 개발 환경과 운영 환경의 설정을 분리할 수 있다.

application.yml

기본으로 사용할 Profile을 지정한다.

spring:
  profiles:
    default: dev

application-dev.yml

server:
  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-nameMariaDB JDBC Driver
spring.datasource.urlDB 접속 주소
spring.datasource.usernameDB 사용자
spring.datasource.passwordDB 비밀번호
mybatis.mapper-locationsMapper 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 저장소에 직접 올리지 않고 환경변수나 별도의 비공개 설정으로 관리해야 한다.


13. JUnit과 Given–When–Then

@SpringBootTest를 사용하면 Spring Application Context를 실행한 상태에서 Spring Bean과 DB 연결을 테스트할 수 있다.

@SpringBootTest
public class UserApplicationTests {

    @Autowired
    private UserMapper userMapper;
}

테스트 코드는 다음 구조로 작성했다.

Given: 테스트에 필요한 데이터 준비
When: 테스트할 기능 실행
Then: 실행 결과 검증

14. 회원 등록 테스트

@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 제약조건이 있다면 테스트를 반복 실행할 때 중복 데이터 오류가 발생할 수 있다는 점도 주의해야 한다.


15. 전체 회원 조회 테스트

@Test
public void list() {

    // when
    List<UserResponseDTO> list =
        userMapper.findByAll();

    // then
    list.forEach(System.out::println);

    Assertions.assertNotNull(list);
    Assertions.assertFalse(list.isEmpty());
}

단순히 결과를 출력하는 것만으로는 테스트 성공 여부를 자동으로 판단하기 어렵다.

따라서 Assertions를 이용해 실제 결과를 검증하는 것이 좋다.


16. 로그인 테스트와 발견한 문제

처음 작성한 테스트에서는 로그인 요청 이메일과 예상 이메일이 달랐다.

// 요청 이메일
.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(
        "이메일 또는 비밀번호가 일치하지 않습니다."
    )
);

17. Swagger와 OpenAPI

Swagger는 REST API의 정보를 문서화하고 브라우저에서 직접 요청을 테스트할 수 있도록 지원한다.

주요 문서화 대상은 다음과 같다.

  • API Endpoint
  • HTTP Method
  • API 기능
  • 요청 파라미터
  • 요청 DTO
  • 응답 DTO
  • HTTP 상태 코드
  • 성공 및 실패 응답

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

18. Swagger 주요 어노테이션

어노테이션적용 위치역할
@TagControllerAPI 그룹 이름과 설명
@OperationController 메서드API 기능 설명
@Parameter메서드 파라미터개별 요청값 설명
@ParameterObjectDTO 파라미터DTO를 Query Parameter로 표현
@SchemaDTO 및 필드데이터 구조 설명
@ApiResponseController 메서드응답 상태와 내용 설명
@ApiResponsesController 메서드여러 응답 상태를 묶어서 명세

@Tag

Controller가 담당하는 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();
}

@Schema

DTO 또는 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();
}

19. @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("이메일 또는 비밀번호가 일치하지 않습니다.");

20. 이번 실습의 전체 흐름

클라이언트
    ↓
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 실행 결과를 검증했다.


새롭게 알게 된 점

  • Spring은 객체 생성을 Bean으로 등록하여 관리할 수 있다.
  • @RestController를 사용하면 Java 객체를 JSON 형태로 응답할 수 있다.
  • 요청 파라미터가 여러 개라면 @RequestParam으로 각각 받거나 DTO로 묶어 받을 수 있다.
  • Service 구현체가 여러 개라면 Bean 이름으로 주입 대상을 선택할 수 있다.
  • MyBatis Mapper는 인터페이스만 작성해도 MyBatis가 프록시 구현 객체를 생성한다.
  • Mapper 인터페이스와 XML은 namespace와 SQL id를 기준으로 연결된다.
  • parameterType은 SQL의 입력 타입이고 resultType은 조회 결과가 변환될 타입이다.
  • #{}는 DTO 프로퍼티 값을 PreparedStatement 파라미터로 바인딩한다.
  • Spring Profile을 사용하면 개발 환경과 운영 환경 설정을 분리할 수 있다.
  • JUnit은 출력만 하는 것이 아니라 Assertion으로 결과를 검증해야 한다.
  • Swagger는 API를 구현하는 도구가 아니라 API의 사용 방법과 응답을 명세하는 도구이다.

헷갈렸던 점

@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)

Swagger에 상태 코드를 작성하면 실제로 반환되는가?

아니다. @ApiResponse는 문서 명세만 담당한다.

실제 상태 코드는 ResponseEntity 또는 예외 처리 코드로 별도로 구현해야 한다.

Optional.get()을 바로 사용해도 되는가?

조회 결과가 없으면 Optional.empty()가 반환되므로 바로 get()을 호출하면 예외가 발생할 수 있다.

먼저 isPresent()로 확인하거나 orElseThrow()를 사용해야 한다.


실습 및 적용

직접 해본 것

  • Spring Initializr로 Gradle 기반 Spring Boot 프로젝트 생성
  • /health/alive Health Check API 구현
  • UserRequestDTO, UserResponseDTO 작성
  • UserService 인터페이스 작성
  • 암호화 방식과 평문 방식의 Service 구현체 작성
  • @Resource(name = "encryption")으로 특정 구현체 주입
  • UserMapper 인터페이스 작성
  • userMapper.xml에 INSERT와 SELECT SQL 작성
  • MariaDB의 SPRING_USER_TBL과 MyBatis 연결
  • 개발용 application-dev.yml 작성
  • JUnit으로 회원 등록, 전체 조회, 로그인 기능 테스트
  • Swagger 어노테이션의 역할과 API 명세 구조 학습

결과

  • Spring Boot 서버를 8000 포트에서 실행하도록 설정했다.
  • /health/alive 요청으로 서버 동작 여부를 확인할 수 있게 되었다.
  • MyBatis Mapper를 통해 회원 등록, 전체 조회, 로그인을 수행할 수 있는 구조를 만들었다.
  • Mapper XML의 SQL과 DTO가 어떻게 연결되는지 확인했다.
  • JUnit의 Given–When–Then 구조로 DB 기능을 검증했다.
  • Swagger를 이용해 API의 요청과 응답을 문서화하는 방법을 익혔다.

문제와 해결

막힌 부분 1: 로그인 테스트의 이메일 불일치

로그인 요청과 예상 결과에서 서로 다른 이메일을 사용했다.

.email("jslim@gmail.com")
Assertions.assertEquals(
    "jslim9413@gmail.com",
    response.get().getEmail()
);

해결 방법

입력 이메일과 예상 이메일을 같은 값으로 수정했다.

.email("jslim9413@gmail.com")

조회 결과가 존재하는지도 먼저 검사했다.

Assertions.assertTrue(response.isPresent());

막힌 부분 2: Optional.get() 사용 시 예외 가능성

로그인에 실패하면 Optional.empty()가 반환되기 때문에 get()을 호출할 때 예외가 발생할 수 있다.

해결 방법

먼저 존재 여부를 검증하거나 orElseThrow()를 사용한다.

UserResponseDTO user = response.orElseThrow(
    () -> new RuntimeException(
        "이메일 또는 비밀번호가 일치하지 않습니다."
    )
);

막힌 부분 3: 운영 Profile 파일 이름

운영 설정 파일을 다음과 같이 작성했다.

application-prod.dev

해결 방법

Spring Boot가 인식할 수 있는 YAML 파일로 수정한다.

application-prod.yml

막힌 부분 4: Service 구현체가 두 개 존재함

UserService를 구현한 클래스가 두 개이므로 타입만으로는 주입 대상을 결정하기 어렵다.

해결 방법

각 Service에 Bean 이름을 지정하고 @Resource로 주입할 Bean을 선택했다.

@Service("encryption")
@Resource(name = "encryption")
private UserService userService;

막힌 부분 5: Swagger 문서와 실제 응답의 차이

@ApiResponse(responseCode = "401")을 작성하면 실제 API도 자동으로 401을 반환한다고 혼동할 수 있다.

해결 방법

Swagger 어노테이션은 명세만 담당하며 실제 동작은 별도로 구현해야 한다.

return ResponseEntity
    .status(HttpStatus.UNAUTHORIZED)
    .body("이메일 또는 비밀번호가 일치하지 않습니다.");

다음에 할 일

  • UserService에서 UserMapper.login()을 실제로 호출하도록 구현하기
  • 로그인 성공과 실패에 따라 적절한 ResponseEntity 반환하기
  • 비밀번호를 평문이 아닌 암호화된 형태로 저장하고 검증하기
  • UserResponseDTO에서 비밀번호 필드 제거하기
  • 회원 가입과 전체 조회 Controller Endpoint 구현하기
  • Swagger UI에서 각 API 직접 테스트하기
  • @PostMapping, @RequestBody를 이용한 JSON 요청 처리 학습하기
  • 공통 예외 처리와 HTTP 상태 코드 적용하기
  • 반복 가능한 DB 테스트 환경 구성하기
  • MyBatis의 resultMap, 동적 SQL, Type Alias 학습하기

25일차를 마치며

오늘은 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 응답까지 구현해야 한다.

profile
개발 공부 기록

0개의 댓글