상세 컨텐츠

본문 제목

[스프링 1팀] 5장~6.5장. API 작성과 데이터베이스 연동

24-25/Spring 1

by oze 2024. 11. 22. 10:00

본문

728x90

 

 

5장. API를 작성하는 다양한 방법

GET API 만들기

GET API: 웹 애플리케이션 서버에서 값을 가져올 때 사용하는 API

 

컨트롤 클래스에 @RestController와 @RequestMapping 설정

    @RestController
    @RequestMapping("/api/v1/get-api")
    public class GetController {

    }
  • 클래스 수준에서 @RequestMapping 설정 -> 내부 선언한 URL 리소스 앞에 @RequestMapping의 값이 공통 값으로 추가됨

 

@RequestMapping으로 구현하기

    // http://localhost:8080/api/v1/get-api/hello
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String getHello() {
        return "Hello World";
    }
  • @RequestMapping은 설정 없으면 HTTP의 모든 요청을 받음
  • method 요소의 값을 RequesetMethod.GET으로 설정 -> 요청 형식을 GET으로만 설정
  • 스프링 4.3 버전 이후로는 @RequestMapping 어노테이션 더 이상 사용 x
  • 아래의 어노테이션 사용
    • @GetMapping
    • @PostMapping
    • @PutMapping
    • @DeleteMapping

 

✅ 매개변수가 없는 GET 메서드 구현

    // http://localhost:8080/api/v1/get-api/name
    @GetMapping(value = "/name")
    public String getName() {
        return "Flature";
    }
  • 주석처리된 URL을 그대로 입력하고 요청 -> 스프링부트 애플리케이션이 정해진 응답을 반환

 

✅ @PathVariable을 활용한 GET 메서드 구현

    // http://localhost:8080/api/v1/get-api/variable1/{String 값}
    @GetMapping(value = "/variable1/{variable}")
    public String getVariable(@PathVariable String variable) {
        return variable;
    }

 

  • 매개변수를 받을 때 자주 쓰이는 방법 중 하나는 URL 자체에 값을 담아 요청하는 것
  • 주석 처리된 예시 URL을 보면 중괄호({})로 표시된 위치의 값을 받아 요청함
  • 값을 간단히 전달할 때 주로 사용, GET 요청에 많이 사용
  • 지켜야 할 규칙
    • @GetMapping 어노테이션의 값으로 URL을 입력할 때 중괄호를 사용해 어느 위치에서 값을 받을지 지정
    • @PathVariable을 명시하고, @GetMapping과 @PathVariable에 지정된 변수의 이름은 동일하게
    // http://localhost:8080/api/v1/get-api/variable2/{String 값}
    @GetMapping(value = "/variable2/{variable}")
    public String getVariable2(@PathVariable ("variable") String var) {
    // = public String getVariable2(@PathVariable (value = "variable") String var) {
        return var;
    }
    • @GetMapping에 지정한 변수의 이름과 메서드 매개변수의 이름을 동일하게 맞추기 어려운 경우 
      • @PathVariable 뒤에 괄호를 열어 @GetMapping의 변수명을 지정 -> variable과 var로 일치x

 

✅ @RequestParam을 활용한 GET 메서드 구현

    // http://localhost:8080/api/v1/get-api/request1?name=value1&email=value2&organization=value3
    @GetMapping(value = "/request1")
    public String getRequestParam1(@RequestParam String name, @RequestParam String email, @RequestParam String organization) {
        return name + " " + email + " " + organization;
    }
  • 앞서 본 URL 경로에 값을 담아 요청을 보내는 방법 외에도 쿼리 형식으로 값 전달 가능
  • URI에서 '?'을 기준으로 우측에 '{키}={값}' 형태로 구성된 요청을 전송
  • 매개변수 부분에 @RequestParam을 명시해 쿼리 값과 매핑
  • '?' 오른쪽에 쿼리스트링(query string) 명시
  • 쿼리스트링에는 키(변수의 이름)가 모두 적혀 있어 이 값을 기준으로 메서드의 매개변수에 이름을 매핑해 값을 가져옴
  • @RequestParam 뒤에 적는 이름을 동일하게 설정하기 어렵다면 @PathVariable에서 본 것처럼 value 요소로 매핑

 

    // http://localhost:8080/api/v1/get-api/request2?key1=value1&key2=value2
    @GetMapping(value = "/request2")
    public String getRequestParam2(@RequestParam Map<String, String> param) {
        StringBuilder sb = new StringBuilder();

        param.entrySet().forEach(map -> {
            sb.append(map.getKey() + " : " + map.getValue() + "\n");
        });

        return sb.toString();
    }
  • Map 객체 
    • 쿼리스트링에 어떤 값이 들어올지 모르는 경우 
    • 값에 상관없이 요청을 받을 수 있음

 

✅ DTO 객체를 활용한 GET 메서드 구현

  • DTO란?
    • DTO: Data Transfer Object. 각 클래스 및 인터페이스를 호출하면서 전달하는 매개변수로 사용되는 데이터 객체
    • 데이터를 교환하는 용도로만 사용하는 객체 -> 별도의 로직 포함 X
  • DTO 클래스 예
    • 전달하고자 하는 필드 객체 선언, getter/setter 메서드 구현
    • 선언된 필드는 컨트롤러의 메서드에서 쿼리 파라미터의 키와 매핑됨
    • -> 쿼리스트링의 키가 정해져 있지만 받아야 할 파라미터가 많을 경우 DTO 객체 활용하면 가독성 높임
public class MemberDto {

    private String name;
    private String email;
    private String organization;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getOrganization() {
        return organization;
    }

    public void setOrganization(String organization) {
        this.organization = organization;
    }

    @Override
    public String toString() {
        return "MemberDto{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", organization='" + organization + '\'' +
                '}';
    }
}

 

  • DTO 객체를 활용한 GET 메서드 구현
    // http://localhost:8080/api/v1/get-api/request3?name=value1&email=value2&organization=value3
    @GetMapping(value = "/request3")
    public String getRequestParam3(MemberDto memberDto) {
        return memberDto.toString();
    }

 

 

POST API 만들기

POST API: 웹 애플리케이션을 통해 데이터베이스 등의 저장소에 리소스를 저장할 때 사용되는 API

  • POST API에서는 저장하고자 하는 리소스나 값을 HTTP 바디(body)에 담아 서버에 전달

 

@RequestMapping으로 구현하기

@RestController
@RequestMapping("/api/v1/post-api")
public class PostController {
    @RequestMapping( value = "/domain", method = RequestMethod.POST)
    public String postExample() {
        return "Hello Post API";
    }
}

 

✅ @RequestBody를 활용한 POST 메서드 구현

  • POST 요청에서는 리소스를 담기 위해 HTTP Body에 값을 넣어 전송
  • Body 영역에 작성되는 값은 일정한 형태를 취함
  • 일반적으로 JSON(JavaScript Object Notation) 형식으로 전송
    // http://localhost:8080/api/v1/post-api/member
    @PostMapping(value = "/member")
    public String postMember(@RequestBody Map<String, Object> postData) {
        StringBuilder sb = new StringBuilder();

        postData.entrySet().forEach(map -> {
            sb.append(map.getKey() + " : " + map.getValue() + "\n");
        });

        return sb.toString();
    }
  • @PostMapping 사용 -> @RequestMapping과 달리 따로 method 요소 정의하지 않아도 됨
  • @RequestBody: HTTP의 Body 내용을 해당 어노테이션이 지정된 객체에 매핑하는 역할

 

DTO 객체를 활용한 POST API 구현

    // http://localhost:8080/api/v1/post-api/member2
    @PostMapping(value = "/member2")
    public String postMemberDto(@RequestBody MemberDto memberDto) {
        return memberDto.toString();
    }
  • MemberDto의 멤버 변수를 요청 메시지의 키와 매핑해 값을 가져옴

 

PUT API 만들기

PUT API: 웹 애플리케이션 서버를 통해 데이터베이스 같은 저장소에 존재하는 리소스 값을 업데이트하는 데 사용. POST API와 비교하면 요청을 받아 실제 데이터베이스에 반영하는 과정에 차이가 있지만 구현하는 방법은 HTTP Body를 활용하기 때문에 거의 동일.

 

@RequestBody를 활용한 PUT 메서드 구현

@RestController
@RequestMapping("/api/v1/put-api")
public class PutController {
    // < Map 객체 >
    // http://localhost:8080/api/v1/put-api/member
    @PutMapping(value = "/member")
    public String postMember(@RequestBody Map<String, Object> putData) {
        StringBuilder sb = new StringBuilder();

        putData.entrySet().forEach(map -> {
            sb.append(map.getKey() + " : " + map.getValue() + "\n");
        });

        return sb.toString();
    }
}
  • POST API와 마찬가지로 HTTP Body에 담아 전달 -> 서버에서 이 값을 받기 위해 @RequestBody를 사용

 

   DTO 객체를 활용한 PUT 메서드 구현

    // http://localhost:8080/api/v1/put-api/member1
    @PutMapping(value = "/member1")
    public String postMemberDto1(@RequestBody MemberDto memberDto) {
        return memberDto.toString();
    }
    
    // http://localhost:8080/api/v1/put-api/member2
    @PutMapping(value = "/member2")
    public MemberDto postMemberDto2(@RequestBody MemberDto memberDto) {
        return memberDto;
    }
  • postMemberDto1: 리턴 값이 String 타입. 결괏값으로 일반 문자열 전달됨.
  • postMemberDto2: 리턴 값이 DTO 객체 타입. 결괏값으로 json 형식이 전달됨.
  • @RequestController이 지정된 클래스는 @ResponseBody를 생략할 수 있는데, 이 @ResponseBody 어노테이션은 자동으로 값을 JSON과 같은 형식으로 변환해서 전달하는 역할

 

ResponseEntity를 활용한 PUT 메서드 구현

  • HttpEntity: Header와 Body로 구성된 Http 요청과 응답을 구성
public class HttpEntity<T> {
	
    private final HttpHeaders headers;
    
    @Nullable
    private final T body;
    
    ...
}

 

  • RequestEntity와 ResponseEntity: HttpEntity를 상속받아 구현한 클래스
  • ResponseEntity: 서버에 들어온 요청에 대해 응답 데이터를 구성해서 전달. HttpEntity로부터 HttpHeaders와 Body를 가지고 자체적으로 HttpStatus를 구현
  • 이 클래스를 활용하면 응답 코드 변경은 물론 Header와 Body를 더욱 쉽게 구성 가능
  • (PUT 메서드에서 소개하고 있지만 다른 메서드에서 모두 사용 가능)
public class ResponseEntity<T> extends HttpEntity<T> {
	
    private final Object status;
    
    ...
}

 

  • ResponseEntity를 활용한 PUT 메서드 구현
    • 리턴 타입을 ResponseEntity으로 설정
    • status에 다양한 값 가능. 예제에선 HttpStatus.ACCEPTED 사용 -> 응답 코드 202
    // http://localhost:8080/api/v1/put-api/member3
    @PutMapping(value = "/member3")
    public ResponseEntity<MemberDto> postMember3(@RequestBody MemberDto memberDto) {
        return ResponseEntity
                .status(HttpStatus.ACCEPTED)
                .body(memberDto);
    }

 

DELETE API 만들기

DELETE API: 웹 애플리케이션 서버를 거쳐 데이터베이스 등의 저장소에 있는 리소스를 삭제할 때 사용

 

✅ @PathVariable을 활용한 DELETE 메서드 구현

@RestController
@RequestMapping("/api/v1/delete-api")
public class DeleteController {
    // < @PathVariable >
    // http://localhost:8080/api/v1/delete-api/{String 값}
    @DeleteMapping(value = "/{variable}")
    public String DeleteVariable(@PathVariable String variable) {
        return variable;
    }
}
  • URI에 포함된 값을 받아 로직 처리
  • @DeleteMapping에 정의한 value의 이름과 메서드의 매개변수 이름을 동일하게 설정

@RequestParam을 활용한 DELETE 메서드 구현

    // http://localhost:8080/api/v1/delete-api/request1?email=value
    @DeleteMapping(value = "/request1")
    public String getRequestParam1(@RequestParam String email) {
        return "e-mail : " + email;
    }
  • @RequestParam을 통해 쿼리스트링 값 받기

 

REST API 명세를 문서화하는 방법: Swagger

  • API를 개발하면 명세를 관리해야 함.
  • API 명세: 해당 API가 어떤 로직을 수행하는지, 어떤 값을 요청하는지, 응답값으로 무엇을 받을 수 있는지 정리한 자료
  • API는 개발 과정에서 계속 변경되므로 작성한 명세 문서도 주기적인 업데이트 필요 -> 번거롭고 오래 걸림
  • 위 문제를 해결하기 위한 오픈소스 프로젝트가 Swagger
  • Swagger에서는 API 명세 관리뿐만 아니라 직접 통신도 시도 가능

📌 pom.xml 파일에 Swagger 의존성 추가

 <dependencies>
	<!-- ...생략... -->
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>2.9.2</version>
    </dependency>

    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger-ui</artifactId>
      <version>2.9.2</version>
    </dependency>
  </dependencies>

 

📌 Swagger 설정 코드

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.springboot.api"))
            .paths(PathSelectors.any())
            .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            .title("Spring Boot Open API Test with Swagger")
            .description("설명 부분")
            .version("1.0.0")
            .build();
    }
}
  • RequestHandlerSelectors.basePackage(): Swagger에서 스캔할 패키지 범위를 설정
  • Swagger 사용을 위한 기본적인 설정 완료
  • -> http://localhost:8080/swagger-ui.html 접속하면 Swagger 페이지 출력

📌 기존 코드에 Swagger 명세를 추가(@RequestParam을 활용한 GET 메서드)

    @ApiOperation(value = "GET 메소드 예제", notes = "@RequestParam을 활용한 GET Method")
    @GetMapping(value = "/request1")
    public String getRequestParam1(
        @ApiParam(value = "이름", required = true) @RequestParam String name,
        @ApiParam(value = "이메일", required = true) @RequestParam String email,
        @ApiParam(value = "회사", required = true) @RequestParam String organization) {
        return name + " " + email + " " + organization;
    }
  • @ApiOperation:대상 API의 설명을 작성하기 위한 어노테이션
  • @ApiParam: 매개변수에 대한 설명 및 설정을 위한 어노테이션. 메서드의 매개변수뿐 아니라 DTO 객체를 매개변수로 사용할 경우 DTO 클래스 내의 매개변수에도 정의 가능

 

로깅 라이브러리: Logback

로깅(logging): 애플리케이션이 동작하는 동안 시스템의 상태나 동작 정보를 시간순으로 기록하는 것.

  • 비기능 요구사항에 속하지만 디버깅하거나 개발 이후 발생한 문제를 해결할 때 원인을 분석하는데 필수
  • Logback: 자바 진영에서 많이 사용되는 로깅 프레임워크
    • spring-boot-starter-web 라이브러리 내부에 내장 -> 별도의 의존성 추가 필요X
  • Logback의 특징
    • 크게 5개의 로그 레벨을 설정 가능
      1. ERROR: 로직 수행 중 시스템에 심각한 문제가 발생해서 작동이 불가능한 경우
      2. WARN: 시스템 에러의 원인이 될 수 있는 경고 레벨
      3. INFO: 애플리케이션의 상태 변경과 같은 정보 전달을 위해 사용
      4. DEBUG: 애플리케이션의 디버깅을 위한 메시지를 표시하는 레벨
      5. TRACE: DEBUG 레벨보다 더 상세한 메시지를 표현하기 위한 레벨
    • 실제 운영 환경과 개발 환경에서 각각 다른 출력 레벨을 설정해서 로그를 확인 가능
    • Logback의 설정 파일을 일정 시간마다 스캔해서 애플리케이션을 재가동하지 않아도 설정을 변경 가능
    • 별도의 프로그램 지원 없이도 자체적으로 로그 파일을 압축 가능
    • 저장된 로그 파일에 대한 보관 기간 등을 설정해서 관리 가능

 

Logback 설정

  • 일반적으로 클래스패스(classpath)에 있는 설정 파일을 자동으로 참조 -> Logback 설정 파일은 리소스 폴더 안에 생성
  • 스프링 부트에서는 logback-spring.xml 파일 참조
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<!-- property 영역 -->
    <property name="LOG_PATH" value="./logs"/>

    <!-- Appenders 영역 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <!-- encoder 영역 -->
        <encoder>
        	<!-- pattern 영역 -->
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="INFO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <file>${LOG_PATH}/info.log</file>
        <append>true</append>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/info_${type}.%d{yyyy-MM-dd}.gz</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!-- encoder 영역 -->
        <encoder>
        	<!-- pattern 영역 -->
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
        </encoder>
    </appender>

    <!-- TRACE > DEBUG > INFO > WARN > ERROR > OFF -->
    <!-- Root Logger -->
    <!-- root 영역 -->
    <root level="INFO">
        <appender-ref ref="console"/>
        <appender-ref ref="INFO_LOG"/>
    </root>
</configuration>

 

📌 Appender 영역

Appender 영역: 로그의 형태를 설정하고 어떤 방법으로 출력할지를 설정하는 곳. 하나의 인터페이스를 의미하며, 하위에 여러 구현체 존재

  • Logback의 설정 파일을 이용하면 위 그림의 각 구현체를 등록해서 로그를 원하는 형식으로 출력 가능
  • Appender의 대표적인 구현체
    • ConsoleAppender: 콘솔에 로그 출력
    • FileAppender: 파일에 로그 저장
    • RollingFileAppender: 여러 개의 파일을 순회하면서 로그 저장
    • SMTPAppender: 메일로 로그 전송
    • DBAppender: 데이터베이스에 로그 저장
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
        </encoder>
... 중략 ...
    <appender name="INFO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
... 중략 ...
        <encoder>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
        </encoder>
  • appender 요소의 class 속성에 각 구현체 정의
  • filter 요소로 각 Appender가 어떤 레벨로 로그를 기록하는지 지정
  • encoder 요소를 통해 로그의 표현 형식을 패턴(pattern)으로 정의
패턴 의미
%Logger{length} 로거의 이름
%-5lebel 로그 레벨. -5는 출력 고정폭의 값
%msg(%message) 로그 메시지
%d 로그 기록 시간
%p 로깅 레벨
%F 로깅이 발생한 애플리케이션 파일명
%M 로깅이 발생한 메서드 이름
%I 로깅이 발생한 호출지의 정보
%thread 현재 스레드명
%t 로깅이 발생한 스레드명
%c 로깅이 발생한 카테고리
%C 로깅이 발생한 클래스명
%m 로그 메시지
%n 줄바꿈
%r 애플리케이션이 실행 후 로깅이 발생한 시점까지의 시간
%L 로깅이 발생한 호출 지점의 라인 수

 

 

📌 Root 영역

Root 영역: 설정 파일에 정의된 Appender를 활용하려면 Root 영역에서 Appender를 참조해서 로깅 레벨을 설정

  • 특정 패키지에 대해 다른 로깅 레벨 설정하고 싶다면 -> root 대신 logger를 사용해 지정 가능
    <root level="INFO">
        <appender-ref ref="console"/>
        <appender-ref ref="INFO_LOG"/>
    </root>
    
    또는 
    
    <logger name="com.springboot.api.controller" level="DEBUG" additivity="false">
    	<appender-ref ref="console"/>
        <appender-ref ref="INFO_LOG"/>
    </logger>
  • logger 요소의 name 속성에는 패키지 단위로 로깅이 적용될 범위 지정
  • level 속성으로 로그 레벨 지정
  • additivity 속성은 앞에서 지정한 패키지 범위에 하위 패키지를 포함할지 여부 결정(기본값 true)

 

Logback 적용하기

  • logback-spring.xml 파일 수정하여 파일로 저장되는 로그 없도록 설정 변경
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <property name="LOG_PATH" value="./logs"/>

    <!-- Appenders -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
        </encoder>
    </appender>

    <!-- TRACE > DEBUG > INFO > WARN > ERROR > OFF -->
    <!-- Root Logger -->
    <root level="INFO">
        <appender-ref ref="console"/>
    </root>
</configuration>

 

Logback은 출력할 메시지를 Appender에게 전달할 Logger 객체를 각 클래스에서 정의해서 사용

  • 앞서 작성한 GetController에 Logger 적용하기
	private final Logger LOGGER = LoggerFactory.getLogger(GetController.class);

    // http://localhost:8080/api/v1/get-api/hello
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String getHello() {
        LOGGER.info("getHello 메소드가 호출되었습니다.");
        return "Hello World";
    }

    // http://localhost:8080/api/v1/get-api/name
    @GetMapping(value = "/name")
    public String getName() {
        LOGGER.info("getName 메소드가 호출되었습니다.");
        return "Flature";
    }
  • Logger 전역 변수로 Logger 객체를 정의
  • Logger는 LoggerFactory를 통해 객체를 생성. 이때 클래스의 이름을 함께 지정해서 클래스의 정보를 Logger에서 가져가게 함
  • info 레벨에서 로그 출력

 

    // http://localhost:8080/api/v1/get-api/variable1/{String 값}
    @GetMapping(value = "/variable1/{variable}")
    public String getVariable1(@PathVariable String variable) {
        LOGGER.info("@PathVariable을 통해 들어온 값 : {}", variable);
        return variable;
    }
  • 로그를 통해 컨트롤러에 들어오는 값 확인 
  • 변수를 지정해 변수로 들어오는 값 로깅. 변수의 값이 들어갈 부분을 중괄호({})로 지정하면 포매팅을 통해 로그 메시지가 구성됨 

 


 

6장. 데이터베이스 연동

마리아DB 설치

📌 다운로드

https://mariadb.org/download/

 

Download MariaDB Server - MariaDB.org

REST API Release Schedule Reporting Bugs … Continue reading "Download MariaDB Server"

mariadb.org

 

ORM

ORM: Object Relational Mapping. 객체 관계 매핑. 객체와 RDB(Relational Database)의 테이블을 자동으로 매핑하는 방법

 

클래스는 데이터베이스의 테이블과 매핑하기 위해 만들어진 것이 아니기 때문에 RDB 테이블과 어쩔 수 없는 불일치가 존재 -> ORM은 불일치와 제약사항을 해결하는 역할

  • 클래스와 데이터베이스의 테이블 매핑(그림)

  • ORM의 장점
    • ORM을 사용하면서 데이터베이스 쿼리를 객체지향적으로 조작 가능
    • 재사용 및 유지보수가 편리
    • 데이터베이스에 대한 종속성 감소
  • ORM의 단점
    • ORM만으로 온전한 서비스를 구현하기엔 한계 존재
    • 애플리케이션의 객체 관점과 데이터베이스의 관계 관점의 불일치 발생
      • 세분성: ORM의 자동 설계 방법에 따라 데이터베이스에 있는 데이블의 수와 애플리케이션의 엔티티 클래스의 수가 다른 경우 발생
      • 상속성: RDBMS에는 상속이라는 개념 X
      • 식별성: RDMBS는 기본키로 동일성을 정의. 하지만 자바는 두 객체의 값이 같아도 다르다고 판단 가능. 식별과 동일성의 문제
      • 연관성: 객체지향 언어는 객체를 참조함으로써 연관성을 나타내지만 RDBMS에서는 외래키를 삽입함으로써 연관성 표현. 객체지향 언어에서 객체를 참조할 때는 방향성이 존재하지만 RDMBS에서 외래키를 삽입하는 것은 양방향의 관계를 가지기 때문에 방향성 X
      • 탐색: 자바와 RDBMS는 어떤 값(객체)에 접근하는 방식이 다름

 

JPA

JPA(Java Persistence API): 자바 진영의 ORM 기술 표준으로 채택된 인터페이스 모음. ORM이 큰 개념이라면 JPA는 더 구체화된 스펙을 포함. JPA 또한 실제로 동작하는 것이 아니고 어떻게 동작해야 하는지 메커니즘을 정리한 표준 명세

  • JPA의 메커니즘을 보면 내부적으로 JDBC를 사용. 개발자 대신 적절한 SQL을 생성하고 데이터베이스를 조작해서 객체를 자동 매핑하는 역할을 수행
  • JPA 기반의 대표적인 구현체
    • 하이버네이트(Hibernate)
    • 이클립스 링크(EclipseLink)
    • 데이터 뉴클리어스(DataNucleus)

 

하이버네이트

하이버네이트: 자바의 ORM 프레임워크. JPA가 정의하는 인터페이스를 구현하고 있는 JPA 구현체.

 

Spring Data JPA

 

Spring Data JPA: JPA를 편리하게 사용할 수 있도록 지원하는 스프링 하위 프로젝트 중 하나. CRUD 처리에 필요한 인터페이스를 제공하며, 하이버네이트의 엔티티 매니저(EntityManager)를 직접 다루지 않고 리포지토리를 정의해 사용함으로써 스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작. 이를 통해 하이버네이트에서 자주 사용 되는 기능을 더 쉽게 사용할 수 있게 구현한 라이브러리.

 

 

영속성 컨텍스트

영속성 컨텍스트(Persistence Context): 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능을 수행.

 

  • 엔티티 객체가 영속성 컨텍스트에 들어오면 JPA는 엔티티 객체의 매핑 정보를 데이터베이스에 반영
  • 엔티티 객체가 영속성 컨텍스트에 들어와 JPA의 관리 대상이 되는 시점부터는 해당 객체를 영속 객체(Persistence Object)라고 부름

 

  • 영속성 컨텍스트는 세션 단위의 생명주기를 가짐
  • 데이터베이스에 접근하기 위한 세션 생성 -> 영속성 컨텍스트 만들어짐 -> 세션 종료 -> 영속성 컨텍스트 없어짐
  • 엔티티 매니저는 이러한 일련의 과정에서 영속성 컨텍스트에 접근하기 위한 수단으로 사용됨

 

엔티티 매니저

엔티티 매니저(EntityManager): 엔티티를 관리하는 객체. 엔티티 매니저는 데이터베이스에 접근해서 CRUD 작업을 수행. Spring Data JPA를 사용하면 리포지토리를 사용해서 데이터베이스 접근하는데, 실제 내부 구현체인 SimpleJpaRepository가 아래와 같이 리포지토리에서 엔티티 매니저를 사용하는 것을 알 수 있음

    public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        this.escapeCharacter = EscapeCharacter.DEFAULT;
        Assert.notNull(entityInformation, "JpaEntityInformation must not be null!");
        Assert.notNull(entityManager, "EntityManager must not be null!");
        this.entityInformation = entityInformation;
        this.em = entityManager;
        this.provider = PersistenceProvider.fromEntityManager(entityManager);
    }

 

  • 엔티티 매니저는 엔티티 매니저 팩토리(EntityManagerFactory)가 만듦. 
  • 엔티티 매니저 팩토리는 데이터베이스에 대응하는 객체로서 스프링 부트에서는 자동 설정 기능이 있기 때문에 application.properties에서 작성한 최소한의 설정만으로도 동작하지만 하이버네이트에서는 persistence.xml이라는 설정 파일을 구성하고 사용해야 하는 객체

 

 엔티티의 생명주기

 

📌 비영속(New): 영속성 컨텍스트에 추가되지 않은 엔티티 객체의 상태를 의미

📌 영속(Managed): 영속성 컨텍스트에 의해 엔티티 객체가 관리되는 상태

📌 준영속(Detached): 영속성 컨텍스트에 의해 관리되었던 엔티티 객체가 컨텍스트와 분리된 상태

📌 삭제(Removed): 데이터베이스에서 레코드를 삭제하기 위해 영속성 컨텍스트에 삭제 요청을 한 상태

 


QUIZ

  1. 매개변수를 받기 위해 URL 자체에 값을 담아 요청할 때 사용하는 어노테이션은 ( @PathVariable )이다.
  2. ( DTO )란 각 클래스 및 인터페이스를 호출하면서 전달하는 매개변수로 사용되는 데이터 객체를 말한다.
  3. 쿼리스트링에 어떤 값이 들어올지 모르는 경우 ( Map ) 객체를 활용하면 된다.
  4. ( Swagger )는 API를 개발하고 명세를 관리하기 위해 개발된 오픈소스 프로젝트이다.
  5. ( 로깅(logging) )이란 애플리케이션이 동작하는 동안 시스템의 상태나 동작 정보를 시간순으로 기록하는 것이고 자바 진영에서 많이 사용되는 로깅 프레임워크는 ( Logback )이다. 

 


PROGRAMMING QUIZ

1. @RequestBody와 Map 객체를 이용해 POST 요청을 보낼 때 다음 코드의 빈칸을 채우시오.

    // http://localhost:8080/api/v1/post-api/member
    // [1번 빈칸]
    public String postMember(// [2번 빈칸] postData) {
        StringBuilder sb = new StringBuilder();

        postData.entrySet().forEach(map -> {
            sb.append(map.getKey() + " : " + map.getValue() + "\n");
        });

        return sb.toString();
    }

 

2. @RequestParam을 활용한 GET 메서드인 다음 코드에 Swagger 명세를 추가하시오.

    // http://localhost:8080/api/v1/get-api/request1?name=value1&email=value2&organization=value3
    @GetMapping(value = "/request1")
    public String getRequestParam1(
    	@RequestParam String name, 
        @RequestParam String email, 
        @RequestParam String organization) {
       return name + " " + email + " " + organization;
    }

1번 답

    // http://localhost:8080/api/v1/post-api/member
    @PostMapping(value = "/member") // 1번 빈칸 답
    public String postMember(@RequestBody Map<String, Object> postData) { // 2번 빈칸 답
        StringBuilder sb = new StringBuilder();

        postData.entrySet().forEach(map -> {
            sb.append(map.getKey() + " : " + map.getValue() + "\n");
        });

        return sb.toString();
    }

 

2번 답

    @ApiOperation(value = "GET 메소드 예제", notes = "@RequestParam을 활용한 GET Method")
    @GetMapping(value = "/request1")
    public String getRequestParam1(
        @ApiParam(value = "이름", required = true) @RequestParam String name,
        @ApiParam(value = "이메일", required = true) @RequestParam String email,
        @ApiParam(value = "회사", required = true) @RequestParam String organization) {
        return name + " " + email + " " + organization;
    }

 


출처: 장정우, 『스프링 부트 핵심 가이드』, 위키북스(2022), p56-104.

Corner Spring 1
Editor:  Minyong

 

728x90

관련글 더보기