BackEnd 3 - Spring Boot, Rest, Service, DTO, Mapper

eve·2025년 12월 17일

✳️ REST

  • Representational State Transfer API
  • 자원의 표현에 의한 상태 전달을 의미한다.
    • 자원 (resource) : 해당 소프트웨어 관리하는 모든 것
    • 표현 (representation) : 그 자원을 표현하기 위한 이름
    • 상태 전달 : 데이터 요청 시점의 자원 상태를 전달 → JSON or XML을 통해 주고받는 것이 일반적이다.
{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com"
}

REST 특징

  • 인터페이스 일관성 (Uniform Interface)
    : 리소스에서 수행 가능한 작업의 균일한 인터페이스를 정의한다.
    GET, POST, PUT, DELETE와 같은 HTTP Method를 사용해 구현된다.

  • 무상태성 (Stateless)
    : 작업을 위한 상태 정보를 따로 저장,관리하지 않는다.
    세션이나 쿠키 정보를 별도로 저장하고 관리하지 않기 때문에, API서버는 단순 요청만 처리한다.
    서비스의 자유도가 증가하고, 불필요한 정보 관리를 하지 않기 때문에 구현이 단순해진다.

  • 캐싱 (Cacheable)
    : REST는 HTTP 웹 표준을 그대로 사용하기 때문에, 웹에서 사용하는 기존 인프라를 그대로 활용할 수 있다.
    HTTP 프로토콜에서 사용하는 Last-Modified 태그나 E-Tag를 이용하면 캐싱 구현도 가능하다.

  • 서버-클라이언트 구조 (Server-Client)
    : 영역을 클라이언트와 서버로 분리한다.
    서버는 처리할 부분이 줄어들고, 여러 플랫폼과 UI 형태로 확장하기 쉬워진다.

  • 계층형 구조 (Layered System)
    : REST 서버는 다중 계층으로 구성될 수 있고,
    로드 밸런싱, 암호화, Proxy 등의 계층을 추가해서 구조상 유연성을 둘 수 있다.


✳️ REST API

: REST의 특징을 기반으로 서비스 API를 구현한 것
각 요청이 어떤 동작이나 정보를 위한 것인지를 '요청 자체'로 추론이 가능하다.

API (Application Programming Interface)
: 응용 프로그램에서 사용할 수 있도록 OS나 프로그래밍 언어가 제공하는 기술을 제어할 수 있게 만든 인터페이스

REST API 설계 예시

  • URI는 자원을 나타내야 하고, 동사보다는 명사를, 대문자보다는 소문자를 사용해야 한다.
  • 일반적으로는 복수 명사로 표현해야 한다. (Collection, Store - 복수 / Document - 단수)
잘못된 예 : http://www.example.com/sendEmail
올바른 예 : http://www.example.com/emails
  • 고유한 객체를 가져올 때는 리소스 옆에 path 상에 id를 나타낸다.
http://www.example.com/emails/:id
  • 마지막에 slash(/)를 포함하지 않는다.
잘못된 예 : http://www.example.com/pages/
올바른 예 : http://www.example.com/pages
  • 언더바(_) 대신 하이픈(-)을 사용한다.
잘못된 예 : http://www.example.com/user_profiles
올바른 예 : http://www.example.com/user-profiles
  • 조인된 결과를 URI 상에 나타내주고 싶을 경우에 사용한다.
    (단, 고유한 값이 리소스 뒤에 나와야 하고 자리는 반드시 지켜야한다.)
잘못된 예 : http://www.example.com/users/user-profiles
올바른 예 : http://www.example.com/user/:id/user-profiles
  • URI에 파일 확장자를 포함하지 않는다.
잘못된 예 : http://www.example.com/documents.pdf
올바른 예 : http://www.example.com/documents
  • 행위를 포함하지 않는다.
    (행위는 URI가 아닌 HTTP 메서드를 통해 나타낼 수 있다.)
잘못된 예 : http://www.example.com/createNewUsers
		http://www.example.com/users/new
올바른 예 : POST http://www.example.com/users

REST API 동작

  • 서버는 클라이언트 요청에 대한 응답으로 화면(view)이 아닌 데이터(data)를 전송한다.
  • 이때 사용하는 응답 데이터는 JSON(Javascript Object Notation) 또는 XML 등이다.

  • 요청/응답 과정
    • 클라이언트 → 서버 (Request)
      : HTTP 요청 메시지는 start-line, header, body로 구성되어 있다.

      • start-line
        : HTTP Method / Request target / HTTP version 으로 구성
        GET /users HTTP/1.1
      • header
        : Host, User-Agent, Authorization, Cookie 등으로 구성
        Host: api.example.com
        User-agent: Mozilla/5.0 (Windows NT 10.0 ...
        Authorization: Bearer eyJhbGciOiJIUzl1N...
        Content-Type: application/json
        Cookie: sessionId=abc123; preferences=dart-mode
        				```
        
      • body
        : HTTP Request가 전송하는 데이터를 담고 있는 부분. 데이터가 없으면 body도 비어있다.

    • 서버 → 클라이언트 (Response)
      : HTTP 응답 메시지도 start-line, header, body로 되어 있다.

      • start-line
        : HTTP version / Status Code / Status Text로 구성
        HTTP/1.1 200 OK
      • header
        : Server, Location 등 응답에 대한 추가 정보
        Content-Type: application/json
        Content-Length: 1024
        Cache-Control: max-age=3600
        Set-Cookie: sessionId=abc123; Expires=Wed, 21 Oct 2023 07:28:00 GMT; Path=/
        				```
        
      • body
        : HTTP Response가 전송하는 데이터를 담는 부분. 데이터가 없으면 body도 비어있다.


✳️ REST API에 사용되는 개념들

*️⃣ DTO (Data Transfer Object)

: 계층 간 데이터 교환을 위해 사용하는 객체
로직을 갖지 않는 순수한 데이터 객체로, Getter/Setter만 가진 클래스이다.

  • DTO는 서버로의 요청(Request)과 서버로부터의 응답(Response) 전송을 위한 용도로 사용한다.

  • DTO가 필요한 이유

    • 클라이언트의 요청 수가 늘어나면, @RequestParam()의 개수가 계속 늘어난다.
    • DTO 클래스는 요청 데이터를 하나의 객체로 묶어서 전달받는 역할을 해준다.
    • "도메인 객체와의 분리"라는 목적이 존재한다.
public class BookDTO {
	// 생성(POST)
	@Getter @Setter
	@NoArgsConstructor
	@AllArgsConstructor
	public static class Post {
		private String title;
		private String subTitle;
		private String author;
		private String publisher;
	}
    // 전체 수정 (PUT)
	@Getter @Setter
	@NoArgsConstructor
	@AllArgsConstructor
	public static class Put {
		private String title;
		private String subTitle;
		private String author;
		private String publisher;
		private Book.Status status;
	}
	// 일부 수정(PATCH)
	@Getter @Setter
	@NoArgsConstructor
	@AllArgsConstructor
	public static class Patch {
		private Book.Status status;
	}
  • DTO에서 validation이 가능하다.
    DTO에 validation을 정의하면 유효성 검사를 통해 잘못된 값을 전달받았을 때 걸러낼 수 있다.
  • validation 의존성 추가해야 함
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Put {
	@NotBlank(message = "제목은 필수 입력 값입니다.")
	@NotNull(message = "제목은 반드시 입력하셔야 합니다.")
	@Size(min = 1, max = 45, message = "제목은 45자 이하여야 합니다.")
	private String title;
	@NotBlank
	@NotNull
	@Size(max = 45)
	private String subTitle;
	@Size(max = 45)
	private String author;
	@Size(max = 45)
	private String publisher;
	private Book.Status status;
}

@PostMapping
public Book insertBook(@Valid @RequestBody BookDTO.Post dto) {
	return bookService.insertBook(dto);
}

*️⃣ Mapper

: DTO와 엔티티 간 매핑을 담당하는 클래스

  • 직접 Mapper 만드는 방법
public class BookMapper {
	// 1. Entity -> DTO
	public BookDTO.Response toDto(Book book) {
		BookDTO.Response bookDto = new
		BookDTO.Response();
		// Entity 필드 값 하나씩 복사
		bookDto.setId(book.getId());
		bookDto.setTitle(book.getTitle());
		bookDto.setAuthor(book.getAuthor());
		bookDto.setPublisher((book.getPublisher()));
		bookDto.setStatus((book.getStatus()));
		return bookDto;
	}
    // 2. DTO -> Entity
	public Book ToEntity(BookDTO.Post postDto) {
		Book book = new Book();
		// DTO 필드 값 하나씩 복사
		book.setTitle(postDto.getTitle());
		book.setSubtitle(postDto.getSubtitle());
		book.setAuthor(postDto.getAuthor());
		book.setPublisher(postDto. getPublisher());
		return book;
	}
}

*️⃣ Mapstruct

: Java 기반의 매퍼 라이브러리 중 하나
컴파일 타임에 매퍼 구현체를 생성해주는 코드 생성기이다.

  • 매퍼 인터페이스를 정의하고, 해당 인터페이스의 메서드를 호출하여 객체 간 매핑 수행 가능
  • mapstruct 의존성 추가해야 함
@Mapper
public interface BookMapper {
	BookDto.Response toDto(Book book);
	Book toEntity(BootDto.Post bookDto);
}

  • 매퍼 인터페이스 정의
    : @Mapper annotation을 사용하면 Mapstruct가 자동으로 구현체를 만들어준다.
    매핑 처리하지 않을 요소는 ignore 속성을 주면 된다.
@Mapper(componentModel = "spring")
public interface BookControlMapper {
	@Mapping(target = "id", ignore = true)
	@Mapping(target = "status", ignore = true)
	Book PostDTOToEntity(BookDTO.Post post);
    
	@Mapping(target = "id", ignore = true)
	void PutDTOToEntity(BookDTO.Put put, @MappingTarget Book book);
    
	@Mapping(target = "id", ignore = true)
	@Mapping(target = "title", ignore = true)
	@Mapping(target = "subTitle", ignore = true)
	@Mapping(target = "author", ignore = true)
	@Mapping(target = "publisher", ignore = true)
	void PatchDTOToEntity(BookDTO.Patch patch, @MappingTarget
Book book);
}
  • 컬렉션끼리 매핑도 가능하다.
    컬렉션 내의 객체들은 순서대로 1:1 매핑된다.
  • @AfterMapping은 매핑 작업이 완료된 이후에 실행되는 메서드나 작업을 의미한다.
    DB 데이터를 Java 객체로 변환 후, 그 객체에서 추가적인 작업을 수행해야 할 때 사용하는 Annotation이다.
@Mapper(componentModel = "spring")
public interface BookResponseMapper {

	BookDTO.Response entityToResponse(Book book);
	List<BookDTO.Response> booksToResponses(List<Book> books);
    
	@AfterMapping
	default void titleAndSubTitle(Book book, @MappingTarget BookDTO.Response response) {
		response.setTitle(book.getTitle() + " - " + book.getSubTitle());
	}	
}

✳️ Service Layer

: 컨트롤러와 리포지토리 사이에 위치하는 계층으로, 서버의 핵심 기능(비즈니스 로직)을 처리하는 순서를 총괄한다.

  • REST Controller는 Spring MVC를 통해 클라이언트 요청을 처리해 응답을 생성한다.
  • Repository는 데이터의 영속성을 관리하고, CRUD 등의 작업을 수행한다.

  • 비즈니스 로직의 분리와 중앙화
    : Service Layer를 도입함으로써 가독성과 유지보수성이 향상되고 개발 효율을 높인다.

  • 역할 분리
    : Presentation Layer, Data Access Layer, Domain Model과 모두 상호작용하며 비즈니스 로직을 처리한다.
    • Presentation Layer : 사용자 요청 처리, 응답 생성
    • Data Access Layer : DB와의 상호작용 담당
    • Service Layer : 비즈니스 로직, 트랜잭션 관리 수행

  • 재사용성
    : Service Layer는 비즈니스 로직을 캡슐화하므로, 애플리케이션 여러 부분에서 재사용성을 촉진한다.

@Transactional

: DB와 관련된, 트랜잭션이 필요한 서비스 클래스 또는 메서드에 사용하는 Annotation

  • 클래스와 메서드 모두 annotation을 적용한 경우, 메서드 레벨의 @Transactional 선언이 우선 적용된다.
  • @Transactional이 붙은 메서드는 포함하는 작업 중 하나라도 실패할 경우, 전체 작업을 취소한다.

  • @Transactional 장점
    • 선언적 트랜잭션 관리
      : 코드 내에서 트랜잭션 관리 로직을 분리할 수 있다. 비즈니스 로직에만 집중할 수 있게 되고, 코드의 가독성과 유지보수성이 향상된다.
    • 일관성 있는 데이터 처리
      : 트랜잭션은 모든 작업이 성공적으로 완료되거나 실패할 경우, 이전 상태로 돌아간다.
      (데이터 무결성 유지에 중요)
    • 간편한 롤백 처리
      : 예외 발생 시, Spring의 트랜잭션 관리가 자동으로 트랜잭션을 롤백한다.
      (개발자가 수동으로 롤백하지 않아도 됨)
    • 트랜잭션의 전파 및 격리수준 제어
      : 트랜잭션의 전파 동작과 격리 수준을 annotation으로 지정할 수 있어, 다양한 시나리오에 유용

  • @Transactional 주의점
    • 프록시 기반 동작
      : 선언적 트랜잭션 관리는 프록시 기반으로 작동한다.
      같은 클래스 내부에서 @Transactional 메서드 직접 호출 시, 트랜잭션 처리가 적용되지 않을 수 있다.
    • 예외처리
      : 런타임 예외와 에러 발생 시에만 롤백한다.
      체크 예외에 대해서는 롤백이 발생하지 않기 때문에, rollbackFor 속성을 사용해 명시적 롤백 조건을 설정해야 한다.
    • 트랜잭션 격리와 데드락
      : 높은 격리 수준 설정 시, 데드락 발생 가능성이 증가할 수 있고,
      낮은 격리 수준 설정 시, 더티리드/팬텀리드 등 다른 트랜잭션 문제가 발생할 수 있다.
      (적절 격리수준 선택해야 함)
    • 성능 고려
      : @Transactional 메서드는 트랜잭션 관리 오버헤드를 발생시킬 수 있다.
      특히 읽기 전용 쿼리에서, readOnly=true 설정으로 성능 최적화 가능

@Transactional 속성

  • Propagation
    : 기본적으로 존재하는 트랜잭션에 참여하거나, 새로운 트랜잭션을 시작한다.

  • Isolation
    : DB의 기본 격리 수준을 따르거나, 특정 격리 수준을 지정할 수 있다.

  • ReadOnly
    : 트랜잭션을 읽기 전용으로 설정해, 데이터 변경을 방지하고 성능을 최적화

  • rollbackFor / noRollbackFor
    : 특정 예외 발생 시, 트랜잭션을 롤백하거나 롤백 않도록 설정

  • Timeout
    : 트랜잭션의 최대 실행시간을 초 단위로 설정해, 시간초과 시 자동 롤백


✳️ RestController

@RestController = @Controller + @ResponseBody


  • @RestController의 모든 메서드에서 리턴되는 값은 "MessageConverter"에서 변환되어, HTTP Response Body에 쓰여진다.

  • 응답할 때, 적절한 상태 코드를 반환하기 위해 ResponseEntity 클래스를 사용한다.

    💡ResponseEntity와 HttpStatus
    : ResponseEntity는 REST 컨트롤러의 응답을 위해 사용하는 클래스,
    HttpStatus는 HTTP 상태 코드를 관리하는 클래스

  • ResponseEntity 클래스에 HTTP 상태 코드, 헤더, 바디를 실어 보낼 수 있다.

@PostMapping("/users")
publiuc ResponseEntity<User> createUser(@RequestBody User user) {
	User savedUser = userService.createUser(user);
    return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}

0개의 댓글