728x90

주제: JPA

참여인원: 2

발표내용:

JPA를 사용하거나 JPA를 사용하여 CRUD작업 시 제대로 고민하고 사용하지 않았거나, 다른 분들은 어떤 문제들을 만나보았고 어떻게 해결을 하였는지를 학습해 보았습니다.

 

기본적으로 ORM이란 무엇인가, JPA와 그리고 Persistemce Context에 대해 다뤄보았으며, 이에 대하여 추가적으로 Querydsl과 같은 프레임워크에 대하여 간단하게 알아보는 시간을 가졌습니다.

 

이에 해당 발표자료와 학습을 할 때 검증한 프로젝트를 기록합니다.

JPA_2024_05_25.pdf
2.79MB

 

728x90
728x90

Spring Framework에서  Bean을 주입받기 위해 lombok에서 제공하는 RequiredArgsConstructord어노테이션을 사용하셔 보셨을 것입니다.

 

하지만 이때 Qualifier를 사용하게 된다면 원하던 Bean을 주입받지 못하는 현상이 발생합니다.

 

이 글에서는 왜 @Quailifier가 정상적으로 작동하지 않는 이유와 정상적으로 작동하려면 어떻게 해줘야 하는지에 대해 알아보도록 하겠습니다.

 

왜 적용이 안되는가?

이를 위해서는 lombok이 어떻게 빈을 주입받을 수 있게 해주는지 즉 작동 방법에 대해 알아볼 필요가 있습니다.

 

우선 우리는 lombok을 Getter, Setter, AllArgsConstructor, Builder 등 우리가 코딩을 해서 만들어도 되지만 컴파일할 때 자동으로 명시한 어노테이션의 기능들을 자동으로 생성되게 도와주는 라이브러리입니다.

위와 같이 컴파일을 거치게 되면 명시된 어노테이션은 사라지고 개발자가 직접 작성하지 않은 코드가 생성되는 것을 확인할 수 있습니다.

 

그럼 이제 어떤 역할을 하는 라이브러리인지 알았으니 오늘의 주제 @RequiredArgsConstructord는 왜 @Qualifier가 적용되지 않는가? 에 대해 알아보도록 하겠습니다.

 

우선 @RequiredArgsConstructord@Qualifier에 대해 모르시는 분들은 아래 글을 보고 마저 봐주시기 바랍니다. 

 

[Spring] Bean은 어떻게 생성하고 주입할까?

우리는 Spring Framework에 대해 많이 들어보았고, 공부했고, 사용을 해보았을 것입니다. 기본적으로 Bean 어노테이션을 사용하여 Bean으로 등록하여 개발자가 직접 생성을 하여 사용하지 않아도, Sprin

lee-geon-exception.tistory.com

 

우선 일반 생성자로 만들고 Compile을 할 경우 아래와 같이 생성자 파라미터에 Qualifier로 어떤 Bean을 주입받을 것인지 명시가 되어있습니다.

@RestController
@RequestMapping({"/connect"})
public class ConnectTestController {
    private final Test bean;

    public ConnectTestController(@Qualifier("test1") Test bean) {
        this.bean = bean;
    }

    @GetMapping
    public String connect() {
        System.out.println(this.bean.text);
        return "OK";
    }
}

 

아래 코드는 @RequiredArgsConstructord를 사용하여 만든 코드를 컴파일한 코드입니다.

@RestController
@RequestMapping({"/connect"})
public class ConnectTestController {
    @Qualifier("test1")
    private final Test bean;

    @GetMapping
    public String connect() {
        System.out.println(this.bean.text);
        return "OK";
    }

    public ConnectTestController(final Test bean) {
        this.bean = bean;
    }
}

 

다른 점이 보이시나요??

위 생성자를 직접 생성하여 Qualifier를 사용한 경우 생성자에 파라미터로 주입받을 때 명시적으로 붙어있지만 lombok에서 지원해 주는 어노테이션을 사용할 경우 final로 Test타입의 bean을 주입받겠다고 작성되어 있습니다.

 

이럴 경우 Bean의 명칭이 달라 test1으로 지정한 Bean을 주입받지 못하고 다른 Bean을 주입받거나 아예 못 받는 경우가 생기는 것입니다.

어떻게 해결할 수 있는가??

우선 가장 쉬운 방법은 위에 보여드린 것처럼 직접 생성자를 작성하는 방법이 있고, @Autowired를 사용하여 필드 주입을 해주는 방법이 존재합니다.

 

하지만 우리가 lombok을 사용하는 가장 큰 이유 코드를 덜 작성하고 싶다!! 는 니즈를 충족하는 방법이 있는데, 이는 lombok.config를 설정하는 것입니다.

 

src/main/java경로에 lombok.config파일을 생성 후 아래 코드를 작성해 주면 lombok에서 compile시 코드를 만들어 줄 때 해당 설정을 통해 Qualifier를 반영하여 생성해 주는 것을 확인할 수 있습니다.

더보기

아래 사이트는 project lombok 공식 사이트 configuration 설정할 수 있는 설정들에 대한 설명입니다.

 

Configuration system

 

projectlombok.org

 

lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier

Compile 결과물

@RestController
@RequestMapping({"/connect"})
public class ConnectTestController {
    @Qualifier("test1")
    private final Test bean;

    @GetMapping
    public String connect() {
        System.out.println(this.bean.text);
        return "OK";
    }

    public ConnectTestController(@Qualifier("test1") final Test bean) {
        this.bean = bean;
    }
}

 

728x90

'Server' 카테고리의 다른 글

[Kafka] 설치하기  (0) 2024.06.07
[스터디] JPA에 대하여  (0) 2024.05.25
[Spring] Bean은 어떻게 생성하고 주입할까?  (0) 2024.05.01
SSE란? - Server Sent Events  (2) 2024.04.18
REST API란?  (0) 2024.04.14
728x90

우리는 Spring Framework에 대해 많이 들어보았고, 공부했고, 사용을 해보았을 것입니다.

 

기본적으로 Bean 어노테이션을 사용하여 Bean으로 등록하여 개발자가 직접 생성을 하여 사용하지 않아도, Spring Context 내에서 Bean을 인식하고 생성하여 관리하도록 하였을 것입니다.

 

이번 글에서 어떻게 사용할 Bean으로 인지하게 할 수 있는지, 또 어떻게 사용할 수 있을지 등을 확인해 보도록 하겠습니다.

Bean 등록

Bean으로 등록하려면 여러 가지 방법이 존재합니다.

1. @Bean

Bean 어노테이션의 경우 개발자가 직접 제어가 불가능한 외부 라이브러리등을 Bean으로 만들려 할 때 사용합니다.

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
    @AliasFor("name")
    String[] value() default {};

    @AliasFor("value")
    String[] name() default {};

    boolean autowireCandidate() default true;

    String initMethod() default "";

    String destroyMethod() default "(inferred)";
}

Bean어노테이션 내부를 보면 위와 같이 구현되어 있으며 사용할 때는 아래와 같이 정의를 할 수 있습니다.

@Configuration
public class TestConfiguration {

    @Bean
    public String testDescription() {
        return "test";
    }
}

위와 같이 정의를 한다면 이제 testDescription이라는 Bean이 생성되는 것이고 이를 사용하려면 @Autowired와 같이 메서드 명으로 주입받아 사용할 수 있습니다.

@Autowired
private String testDescription;

 

2. @Component

개발자가 개발한 Class를 Bean으로 등록하기 위해 사용하는 어노테이션입니다.

 

@Bean어노테이션과 다르게 Class자체를 Bean으로 등록하는 것으로, 위의 @Bean어노테이션을 사용하여 Bean으로 등록해도 괜찮지만, 개발자가 제어가 가능하여 @Component를 사용합니다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}

어노테이션 자체는 위와 같이 구현되어 있으며, Class, interface, enum 등에 지정할 수 있습니다.

 

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    @Autowired
    private Test testDescription;

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

위 controller를 보면 Test라는 클래스를 Bean으로 등록하고, 이를 사용하는 부분인데 , Bean과 다르게 변수명을 클래스 명과 다르게 명시하였음에도 정상적으로 주입받는 것을 확인할 수 있습니다.

 

이를 통해 @Component어노테이션을 사용하여 Bean으로 등록할 경우 주입받는 Type에 맞춰 주입받는 것을 확인할 수 있습니다.

 

Bean 주입

위에 글에서도 나왔듯 @Autowired처럼 Bean들을 주입받을 수 있는 방법들이 존재며,

이는 생성자 주입(Constructor Injection), 필드 주입(Field Injection), 수정자 주입(Setter Injection)등이 존재합니다.

1. Constructor

해당 객체가 Bean으로 등록될 때 생성자에 명시되어 있는 파라미터들을 즉 Bean들을 주입받는 방식입니다.

 

가장 기본적이라 생각하며, 가장 코드 줄이 긴 방법이지만 가장 직관적인 만큼 오류가 가장 발생하지 않는 방법입니다.

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    private final Test testDescription;

    public ConnectTestController(Test testDescription) {
    	//주입받는 부분
        this.testDescription = testDescription;
    }

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

2. @RequiredArgsConstructor

lombok의 @RequiredArgsConstructor를 사용하는 방법입니다.

@RestController
@RequestMapping("/connect")
@RequiredArgsConstructor
public class ConnectTestController {
    //
    private final Test testDescription;

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

위와 같이 생성자를 직접 명시하지 않고 사용하는 방법이며 final로 지정된 전역변수들에 한해서 빈을 자동으로 주입해 줍니다.

3. @Autowired

위 방법들과 다르게 필드 주입 혹은 수정자 주입받을 때 사용하는 방법입니다.

기본적으로 Springframework에서 제공해 주는 @Autowired어노테이션을 사용합니다.

필드 주입

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    @Autowired
    private Test testDescription;

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

수정자 주입

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    private Test testDescription;

    @Autowired(required = false)
    public void setTestDescription(Test testDescription) {
        this.testDescription = testDescription;
    }

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

위처럼 두 가지 방법으로 구현이 가능한 방법입니다.

 

생성자 주입방식보다 번거롭고 코드 줄이 길어 가독성이 불편하며, 개발자 실수가 많이 이뤄질 수 있는 방법으로 권장하는 Bean주입 방법은 생성자 주입방법을 권장하고 있습니다.

 

 

기타

@Qualifier

Qualifier은 동일한 타입의 Bean들이 많을 때 어떤 Bean을 주입받을지 명시할 때 사용하는 어노테이션입니다.

@Component
public class Test {

    public String text = "test";

    @Bean
    @Qualifier("test1")
    public Test testDescription() {
        Test test =  new Test();
        test.text = "hello";
        return test;
    }
}

우선 Bean을 2개 지정해 보도록 하겠습니다. 하나는 Test라는 Bean이고 하나는 test1이라는 Bean을 등록하였습니다.

위처럼 @Qualifier를 지정하여 사용할 경우 해당 이름으로  Bean이 생성됩니다.

 

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    @Autowired
    @Qualifier("test1")
    private Test bean;
    @Autowired
    private Test test;

    @GetMapping()
    public String connect() {
        System.out.println(bean.text);
        System.out.println(test.text);
        return "OK";
    }
}

 이후 위처럼 각각 정의를 하고 실행을 하였을 때 아래와 같은 실험 결과를 얻어볼 수 있습니다.

 

이처럼 같은 타입을 반환하는 Bean들 중 하나를 지정하여 주입받을 때 사용하는 어노테이션입니다.

728x90

'Server' 카테고리의 다른 글

[스터디] JPA에 대하여  (0) 2024.05.25
[lombok]RequiredArgsConstructord와 Qualifier  (0) 2024.05.03
SSE란? - Server Sent Events  (2) 2024.04.18
REST API란?  (0) 2024.04.14
[Docker] 도커로 데이터 베이스 편하게 사용하자~  (1) 2024.04.08
728x90

최근 지금 까지 해온 것들을 되돌아보며 제가 3년차 개발자가 되기까지 많은 작업을 하였지만,

막상 저를 표현할 수 있는 것들이 준비되어있지 않다고 생각하였습니다.

 

제가 비록 거의 3년간 바쁘게 살아온 거는 사실인거 같습니다. 블로그도 회사 일정에 밀려 중단한 적도 많았고, 사이드 프로젝트, 스터디 등등 시작을 하였다가 일정이 안맞아서, 야근해야해서 등등의 이유로 실패한 적도 많은 것도 사실입니다. ( 물론 힘들어서 못한 점도 있습니다. )

 

다만 힘든 일정속에서도 계속하여 자기개발에 힘을 써왔고, 이제는 이를 증명하기 위해 그리고 저의 기술을 다듬고 제가 하고 싶은 개발을 하기 위해 사이드 프로젝트를 시작합니다.

 

프로젝트가 끝날때까지 프로젝트 진행 사항을 블로그에 글로 남김으로써 어떤 부분을 고민하였고, 어떤 문제점들을 만났으며, 이를 어떻게 해결하였는지 등이 잘 나타날 수 있게 글을 작성할 것이며, 시작은 기획부터 시작하겠습니다.

 

728x90
728x90

SSE(Server Sent Events)란 서버 주체 단방향 통신 기술입니다.

이것이 무슨 말이냐 우리가 기본적으로 사용하는 일반적인 HTTP 통신은 Client가 요청을 보내면 서버는 해당 요청 건에 대해서만 응답을 할 수 있는 규칙을 가지고 있습니다.

 

이럴 경우 우리가 개발한 서버에서 어떤 작업이 이뤄졌을 때 Client가 알 수 있는 방법은 Client의 요청이 있을 때까지 기다리는 방법 밖에는 없습니다. 하지만 이렇게 구현을 하자면 Client는 일정 시간이 지날 때마다 서버를 조회하여 데이터를 최신화 시켜주는 방법 밖에 없을 것이고 이는 무분별한 호출로 인한 리소스 낭비로 인해 문제를 야기할 수도 있습니다.

 

그럼 서버에서 응답을 보내서 Client에서 처리하면 어떨까? 해서 나온 것이 웹소켓과 SSE기술입니다.

웹소켓의 경우 양방향 통신으로 handshaking방식으로 이루어집니다. 이는 Client와 서버의 통신이 연결되는 방식을 말하며 웹소켓에서 한번 연결된 통신은 지속적으로 유지되며, 이 통신을 통해 Client와 서버는 데이터를 주고 받을 수 있습니다. 이는 채팅과 같이 실시간 소통이 이뤄져야하는 기능을 구현 할 때 많이 사용되는 방법입니다.

 

하지만 알람과 같이 Client가 서버로 요청을 보내지 않아도 서버에서 특정 작업이 생겼을 때 Client가 알게 하고 싶다면 WebSocket방식은 과한 기능 구현인 것 같을 수 있습니다. 그럴때는 SSE를 적용하는 것을 고민해볼 수 있습니다.

 

SSE는 Client의 요청을 기다리지 않고 서버에서 Client로 요청을 보내는 기술로 서버에서 단방향 통신으로 Client로 응답을 보낼 수 있습니다. Client가 처음 한번의 요청을 통해 Client와 서버의 연결이 설정된 후 서버의 일방적인 데이터 전송이 가능해 집니다.

 

사용방법(Client - React)

Client의 설정은 간단합니다. 위에서 말했듯 Client에서의 작업은 초기 연결 설정을 위한 요청과 Event를 수신하여 작업할 부분만 작업해주면 됩니다.

 

useEffect(() => {
    let eventSource: EventSource;
    if (requestId.length) {
    	// 서버와 연결
        eventSource = new EventSource(defaultURL + "/connect/" + requestId);
        // 이벤트 수신 시 수행할 작업들
        eventSource.addEventListener("message", handleEventMessage);
        eventSource.addEventListener("error", handleEventError);
    }

    return (() => {
        if (requestId) {
        	// 페이지가 종료될 때 설정한 이벤트들도 제거
            eventSource.removeEventListener("message", handleEventMessage);
            eventSource.removeEventListener("error", handleEventError);
        }
    })
}, [requestId]);

위 설정은 서버와의 초기 연결을 요청 보내는 부분입니다.

new EventSource()는 초기 서버 연결을 담당해주는 부분이며, 내부 파라미터로 url을 주입해주면 해당 URL로 Get요청을 보내줍니다.

 

이때  요청 Header의 Content-Typetext/event-stream으로 지정되어 있습니다.

 

사용방법(Back-Spring)

서버는 Client보다 작업량 자체는 많지만 다른 웹소켓과 같은 기술들에 비하면 훨씬 적은 설정으로 구현할 수 있습니다.

 

우선 서버는 Client로부터 연결을 위한 요청이 들어온다면 HTTP Method와 produces즉 미디어 타입을 명시 해주어야합니다.

@GetMapping(value = "/connect/{requestId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@PathVariable String requestId) {
    // SseEmitter설정을 위해 sseService의 register을 사용
    return sseService.register(requestId);
}
더보기

SseEmitter란?

SseEmitter란 Spring framework에서 sse기술을 적용해 관리하기 위하여 지원해주는 객체로써 해당 객체를 사용함으로써 개발자는 손쉽게 Sse기술을 적용할 수 있습니다.

 

해당 객체가 상속받은 ResponseBodyEmitter를 보면 하나 이상의 객체가 응답에 기록되는 비동기 요청을 처리하기 위한 컨트롤러 반환 타입이라고 명시가 되어있습니다.

 

이런 객체를 상속받아 구현된 객체인 만큼 비동기를 지원하며,  Spring framework 4.2부터 사용가능합니다.

@Service
public class SseService {
    //
    private final long TIMEOUT_MILLISEC = 60L * 60 * 1000;
    private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
    
    public SseEmitter register(String requestId) {
        SseEmitter emitter = new SseEmitter(TIMEOUT_MILLISEC);
        config(emitter, requestId);
        emitterMap.put(requestId, emitter);
        return emitter;
    }    

    private void config(SseEmitter emitter, String requestId) {
        emitter.onCompletion(() -> emitterMap.remove(requestId));
        emitter.onTimeout(() -> emitterMap.remove(requestId));
    }
}

sseService의 register구현 부분입니다.

생성시 타임 아웃을 지정할 수 있으며, 해당 값은 연결된 이후 얼마나 시간을 유지 할 것인 지를 명시합니다.

해당 시간이 지나 요청을 할경우 onTimeout() 메소드가 자동합니다.

 

ConcurrentHashMap를 사용하여 inMemory로 관리를 하였으며, Thread safe인 자료구조를 사용하여 통신 연결이 MultiThread환경에서 공유되도록 구현하였습니다.

더보기

※ 주의

여러개의 Was환경을 구축한 경우 Redis를 활용하여 동기화되도록 구현 해야합니다.

위 설정을 통해 Client와 서버의 통신은 지속적으로 연결이 되며, 서버에서 client로 계속 데이터를 보낼 수 있는 환경이 구축 된 것 입니다.

 

Event send

서버에서 이벤트를 보내려면 기존 emitterMap에 저장해 놓은 sseEmitter를 사용하여 메시지를 보내게 됩니다.

public <E> void send(SseEmitter emitter, E data) {
    try {
        emitter.send(
                SseEmitter
                        .event()
                        .name("message")
                        .data(data)
        );
    } catch (IOException e) {
        emitter.completeWithError(e);
        log.error(e.getMessage(), e);
    }
}

emitter를 사용하여 send를 하게 되면 연결설정 되어있는 브라우저로 이벤트가 날라가게 되고, 

위와 같이 지속적으로 웹브라우저 EventStream에 이벤트가 쌓이게 됩니다.

 

SseEmitter.event()에는 이벤트에 대해 설정을 할 수 있습니다.

 - .id(): Event의 ID를 설정할 수 있습니다.

 - .name(): Event의 Name을 설정할 수 있습니다. 이 이름은 Client에서 EventListener가 event를 식별하여 가져가는 기준이 됩니다.

 - .data(): Event에 담겨있는 데이터입니다.

 

해당 통신을 종료하려면 complete()를 사용해야 하며, 통신 종료를 해주지 않으면 timeOut될 때까지 통신이 유지되어 메모리 누수 현상이 발생할 수도 있습니다.

사용시 주의

Sse는 Http1의 버전에서는 브라우저 6개에 한해서 지원해주는 기능입니다.

이는 브라우저 6개가 통신 연결이 되어있다면, 더이상의 연결이 불가한 사항인데, 이러한 점을 브라우저에서는 지원을 해주지 않고, 개선도 안되는 것으로 알고 있습니다.

 

하지만 Http2버전 부터는 100개까지 지원이 가능하다 하니 사용하시는 Http버전에 잘 맞춰 사용하시기 바랍니다.

 

2024.04.19 update

Font에서 EventSource 객체를 사용할 때 주의점

EventSource객체를 통해 통신 연결을 시도하게 되면 서버에서 complete를 하더라도 계속해서 연결을 시도하려합니다.

즉 작업이 끝나 통신을 서버에서 끊었음에도 API를 계속 호출하는 상황이 발생합니다.

 

이를 방지하기 위해 서버와 Client는 종료되는 부분을 명확하게 정하여 EventSource를 close해주시기 바랍니다.

728x90
728x90

오늘은 살짝 신기한 에러 상황을 만나게 되어 가져와 보았습니다.

서버에서 RequestBody로 데이터를 받아 처리를 하거나 아니면 시스템상 타입을 추론하기 어려운 상황에서 발생할 수 있는 오류입니다.

 

문제의 해결은 문제를 이해하는 것부터 시작한다는 말이 있습니다.

그 말대로 우리는 오류를 직접 발생시켜 봄으로써 해당 오류를 더욱 자세하게 알아보도록 하겠습니다.

1. 데이터를 받을 객체 정의

시스템상 데이터 타입을 추적하지 못하는 케이스를 만들기 위해서는 데이터를 담을 수 있는 특정 타입 즉 클래스가 필요하기 때문에 하나 생성해 주도록 합니다.

public class TestData {

    private String name;
    private String description;
    private TestField field;
}

 

 

2. 테스트

public static void main(String[] args) throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    String data = "{\"name\": \"홍길동\", \"description\": \"데이터 파싱 테스트입니다.\", \"field\": {\"data\": \"파싱이 될까요?\"}}";
    Object dd = objectMapper.readValue(data, Object.class);
    TestData field = ((TestData)dd);
}

위와 같이 작성하여 테스트를 진행해 보시면 바로 아래와 같은 오류가 발생하는 것을 확인하실 수 있습니다.

 

문제 발생원인

여기서 왜 오류가 발생했는지 그리고 사용한 적도 없는 LinkedHashMap이 갑자기 왜 나온 것인이 의아할 수 있습니다.

이는 직렬화와 역직렬화를 도와주는 Jackson에 의해 발생하는 오류입니다.

 

역직렬화를 할 경우 Jackson은 데이터 타입을 받아 해당 데이터 타입으로 역직렬화를 진행하게 되는데 Jackson이 객체의 타입이 불명확하게 인지할 경우 기본적으로 LinkedHashMap을 사용하여 객체로 만들어 주게 됩니다. 

실제로 위 코드에서 dd 로 정의한 객체의 타입을 찍어보면 위와 같이 LinkedHashMap으로 정의되어 있는 것을 확인할 수 있습니다.

이러한 상황은 전부 Jackson이 객체 타입을 추론하지 못해 발생한 오류 상황인 것입니다.

해결방안

해결방법은 생각보다 간단합니다.

그저 데이터를 역직렬화할 때 명확한 데이터 타입을 명시해 주면 되는 것입니다.

 

위에 작성한 코드로만 본다면  아래와 같이 타입을 Object처럼 포괄적인 의미가 아닌 명확한 객체 타입을 정해주면 해당 오류가 발생하지 않습니다.

{
	...
    TestData dd = objectMapper.readValue(data, TestData.class);
    ...
}

 

다른 에러 발생 케이스

하지만 위에 작성된 케이스는 오류를 일부러 발생하기 위해 간단하게 테스트한 경우이고 실제 업무에서는 어떤 상황일 때 발생할까요?

아무래도 실제 사용하는 케이스가 필요하다 생각합니다.

 

그리하여 일반적인 API호출하는 것처럼 Get API를 하나 만들어 주도록 하겠습니다.

@RestController
@RequestMapping("/error")
public class ErrorController {

    @Autowired
    private ObjectMapper objectMapper;

    @GetMapping
    public QueryData<TestData> test() throws JsonProcessingException {
        String data = "{\"name\": \"홍길동\", \"description\": \"데이터 파싱 테스트입니다.\", \"field\": {\"data\": \"파싱이 될까요?\"}}";
        QueryData<TestData> queryData = new QueryData<>();
        queryData.setResponse(objectMapper.readValue(data, TestData.class));
        return queryData;
    }
}

일반적인 값을 반환해주는  API입니다.

public class QueryData<T> {
    private T response;
}

Response를 Wrapping할 때 사용한 클래스입니다.

단순하게 response 를 담기 위해 만들었습니다.

 

@SpringBootTest
@AutoConfigureMockMvc
public class ErrorControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void dataParsingTest() throws Exception {
        mockMvc.perform(get("/error").contentType(MediaType.APPLICATION_JSON))
                .andDo((result) -> {
                    QueryData<TestData> queryData = objectMapper.readValue(result.getResponse().getContentAsString(Charset.defaultCharset()), QueryData.class);
                    //에러 발생 부분
                    TestData testData = queryData.getResponse();
                })
                .andExpect(status().isOk());
    }
}

테스트 코드입니다.

SpringBoot환경에서 Mock을 사용하여 테스트 진행하였습니다.

 

위 테스트 코드를 실행해 보시면 response로 받은 데이터를 역직렬화하여 사용하였으나 제너릭에 명시한 타입으로 QueryData의 response가 TestData가 아닌 LinkedHashMap으로 타입이 정의되어 캐스팅할 때 에러가 발생하는 것을 확인할 수 있습니다.

 

제가 경험했던 오류 케이스도 이와 같았으며, Response Data가 한번 객체로 감싸져 있으며, 타입이 명확하지 않아 Object로 인지되어 역직렬화가 된경우 데이터를 꺼내 사용할 때 캐스팅이 이뤄지게 되는데 이때 에러가 발생합니다.

 

또다른 해결 책입니다.

ObjectMapper의 convertValue를 사용하여 

TestData testData = objectMapper.convertValue(queryData.getResponse(), TestData.class);

위와 같이 코드를 수정할 경우 오류 없이 정상적으로 데이터를 꺼내 사용할 수 있으니 편한 방법으로 사용하시면 될 것 같습니다.

728x90

'Error' 카테고리의 다른 글

[Mysql] max_allowed_packet  (0) 2024.04.08
728x90

서버 개발을 하면서 REST API를 많이 접해볼 수 있습니다.
오늘은 이 REST가 무엇이며 일반 API와 REST API는 어떤 차이점을 가지고 있는지 알아보도록 하겠습니다.

REST란?

우선 REST API에 대해 알려면 REST가 무엇인지부터 알아야 합니다.

REST의 FullName은 REpresentational State Transfer인데, 직역하자면 '대표 상태 전송'이라고 해석할 수 있습니다.

REST는 HTTP프로토콜을 사용하며 분산형 하이퍼미디어 시스템의 아키텍처 스타일입니다. 

더보기

분산 하이퍼미디어 시스템(distributed hypermedia systems)이란?

여러 대의 컴퓨터나 네트워크로 구성된 시스템에서 하이퍼 미디어를 관리하고 제공하는 시스템

 

하이퍼미디어는 텍스트, 그래픽, 오디오, 비디오 등 다양한 멀티미디어 자료를 연결하여 구성한 것을 의미합니다.

즉 시스템 간 정보를 주고받는 시스템을 구축할 때 적용하는 아키텍처 스타일이라고 쉽게 표현할 수 있습니다.

이는 프로토콜이나 표준이 아니고 네트워크를 통해 애플리케이션을 느슨하게 설계하기 위한 지침으로 굳이 안 지켜도 됩니다.

다만 REST스타일을 벗어나게 된다면 REST API가 일반 API가 될 뿐입니다.

 

REST 구성

REST는  HTTP를 사용한 아키텍처 스타일이다 보니 HTTP에서 제공하는 자원들을 활용합니다.

1. 자원 - HTTP URL

REST에서 URL은 어디로 갈 것인지 혹은 어떤 정보를 가지고 있는지 즉 정보의 자원을 명시합니다.

즉 URL에는 행위에 관련된 표현이 들어가는 것은 부적절하며, 명사가 들어가는 것이 좋습니다.

2. 행위 - HTTP Method

REST는 HTTP Method로 어떤 행위를 할 것인지를 표현합니다.

행위는 보통 5가지를 사용하며, 이 행위에 대한 것은 아래 표로 정리해 보았습니다.

Method Action
GET 리소스를 조회
POST 리소스 생성
PUT 리소스 수정
PATCH 리소스 부분 수정
DELETE 리소스 삭제

3. 표현 - HTTP Message Pay Load

통신에서 반환된 리소스를 반환하며 리소스는 JSON or XML or HTML 등의 타입을 가집니다.

이때 통신 상태에 대한 HTTP Status가 반환되는데 코드에 대한 의미는 다음과 같습니다.

Code Description
1XX 서버가 요청을 받았으니 클라이언트는 작업을 계속 진행할 것을 의미
2XX 요청을 완료하였음을 의미
3XX URL이 변경되었거나 다른 URL을 호출 할경우
4XX Client에서 서버를 호출할 때 잘못된 호출을 할 경우
5XX 서버에서 요청을 처리 혹은 서버 자체가 잘못 되었을 경우

REST의 제약 조건

REST를 적용하려면 몇 가지 조건이 지켜져야 RESTful하다 할 수 있는데 어떤 조건들이 있는지 알아보겠습니다.

1. Client-Server

클라이언트와 서버의 완전 분리입니다.

클라이언트와 서버를 분리하여 개발을 함으로써 서로 변경사항에 대해 영향을 주지 않기 위함입니다.

 

클라이언트는 URI정보를 통해 서버를 호출할 수 있어야 하며,

서버는 HTTP를 통해 들어온 요청에 대해 데이터를 전달할 수 있습니다.

2. Stateless

Stateless 즉 무상태는 서버에서 클라이언트의 상태에 대한 정보를 가지고 있지 않다는 의미입니다.

이는 Stateless의 반대인 Stateful을 보면 더욱 쉽게 이해할 수 있습니다.

 

Stateful은 서버가 클라이언트의 상태를 가지고 있다는 것을 의미하며, 이는 클라이언트와 서버의 통신은 연결되어 있어 작업을 할 때 서버에서 클라이언트 상태를 보고 작업을 하는 것입니다. 이럴 경우 통신 중이던 서버에 장애가 생길 경우  클라이언트는 그동안 서버에 저장된 상태정보를 유실하게 되어 처음부터 통신을 시작해야 하는 경우가 생깁니다.

 

Stateless는 서버가 클라이언트의 상태를 가지고 있지 않아 클라이언트가 서버를 호출할 때마다 필요한 상태 정보들을 같이 넘겨줘야 한다는 번거로움이 존재합니다. 하지만 위의 경우처럼 서버가 장애가 생겼을 때 같은 기능을 하는 다른 서버가 해당 작업을 이어받아 처리할 수 있게 할 수 있습니다. 즉 서버 확장에 대해 Stateful에 비해 자유롭다고 볼 수 있습니다.

3. Cache

서버에서 응답을 줄 때 해당 리소스의 캐싱 가능 여부와 응답 캐싱기간을 알려주는 정보가 포함되어있어야 하며, 캐싱할 수 있어야 합니다.

이는 클라이언트와 서버의 성능을 향상하는데 도움을 줍니다.

4. Uniform Interface

Uniform Interface는 REST API인가?를 결정하는 가장 중요한데, 이는 클라이언트와 서버의 통신이 일관되고 표준화된 방식이 되도록 유도하는 요소입니다.

 

Uniform Interface는 4가지로 구성되어 있습니다.

- Identification of resources

각 리소스들은 고유한 URI를 가져 식별되어야 합니다.

쉽게 말하면 API를 생성할 때 URI는 고유하게 식별되어 위치를 특정할 수 있어야 한다는 의미입니다.

 

- Manipulation of resources though represenations

이것을 이해하기 위해선 우리는 우선 Represenation에 대해 알아야합니다.

GET http://localhost:8080/helth
Accept:text/plain
Accept-Language:en-US;q=0.8,en;q=0.7

위와 같이 서버가 잘 작동하고 있는지 확인하는 API를 호출한다고 가정해보겠습니다.

그럼 우리가 전달받는 리소스는 "OK"라는 데이터를 받을 수 있을 것 입니다.

하지만 이는 잘못된 설명이였습니다.

 

그 이유는 "OK"라는 것은 리소스가 아닌 represenation data이기 때문입니다.

왜 리소스가 아니냐면 우리는 "OK"라는 데이터를 응답으로 받았지만 여기서 리소스는 "확인을 위한 의미를 담은 문서"이기 때문입니다.

즉 represenation data는 'text/plain' 타입의 영어로 된 데이터가 되는 것 입니다.

 

그리하여 클라이언트는 서버에 'text/palin'을 표현해주길 원하면 텍스트 데이터를, 'text/html'을 표현해주길 원하면 html로 된 데이터를 전달 받을 수 있는 것 입니다. ( 물론 여러 표현방법에 대한 API는 서버에 구현이 되어있어야 합니다 안할경우 406error!! )

 

그럼 Manipulation of resources though represenations는 무슨 의미인가?

요청하는 URI는 동일하지만 표현방법에 따라 응답할때 전달하는 represenation Data 가 둘이상 지원된다는 의미입니다.

즉 URI가 같더라도 원하는 Represenation에 따라 여러 데이터를 응답 받을 수 있다로 설명이 가능할 것 같습니다. 

 

- Self-descrive messages

각 응답과 요청은 스스로 설명이 가능해야 합니다.

이게 무슨 의미냐면, 응답받은 데이터들이 JSON으로 왔다고 가정해 보겠습니다.

{
    "name": "hong gil dong",
    "description": "This is a living thing."
}

위와 같은 리소스를 응답으로 받았다면, 우리는 저 name이 사람이름인지 동물이름인지 알 수 있는 방법이 없을 것입니다.

이럴 때 응답만을 보고 저게 사람이름인지 동물이름인지 알 수 있게 자체적으로 설명이 있어야 한다는 뜻입니다.

 

Spring REST Docs와 같은 프레임워크를 활용하여 응답 리소스에 Doc의 link를 실어 보내주는 방법으로 해결을 해야합니다.

 

- Hypermedia as the engine of application state ( HATEOAS )

클라이언트는 서버로부터 전송받는 응답으로 다음 가능한 행위를 결정할 수 있어야합니다.

이게 무슨 뜻인가!

이전에 우리는 이름과 설명을 응답으로 받았습니다.

여기에 설정을 붙여보겠습니다.

우리가 만약 사람들 관리 사이트에서 유저 상세 화면을 개발하고 있다고 가정을 해보겠습니다.

이때 우리는 기본적으로 해당 유저에 대한 정보들을 가져오게 될 것 입니다.

{
    "name": "hong gil dong",
    "age": 27,
    "phoneNumber": "010-0000-0000"
}

이제 우리는 저 정보들을 가지고 개발을 하게 될 것입니다.

하지만 우리가 페이지 내에서 다음 사람을 보고 싶다거나 할 때 어떤 행위를 해야할 지 위 응답에는 포함되어있지 않습니다.

 

{
    "name": "hong gil dong",
    "age": 27,
    "phoneNumber": "010-0000-0000",
    "links": [
    	{
        	"rel": "self",
            	"href": "http://localhost:8080/user/2"
        },
    	{
        	"rel": "befor",
            	"href": "http://localhost:8080/user/1"
        },
    	{
        	"rel": "after",
            	"href": "http://localhost:8080/user/3"
        }
    ]
}

위와 같이 Hateoas를 적용한다면 이전 유저를 조회하려면 어떤 행위를 해야하는지, 이후 유저를 조회하려면 어떻게 해야하는지에 대한 정보들이 응답에 포함되어있습니다.

 

이로인해 클라이언트는 다른 정보 없이 응답받은 정보들 속에서 다음 행위를 결정할 수 있게 되는 것 입니다.

5. Layered System

클라이언트나 서버 사이 중간계층들을 둘 수 있도록 확장성을 열어 놓은 것을 의미합니다.

미들웨어, 프록시, 게이트웨이, 캐시 서버, 로드 밸런서 등이 이에 포함되며, 이렇게 중간계층이 추가된다 하더라도 클라이언트와 서버의 상호 작용에는 영향이 없어야합니다.

 

6. Code-On-Demand ( Optional )

REST 조건이긴하지만 필수 제약 조건은 아닙니다.

클라이언트가 서버에 코드를 요청시 서버는 클라이언트에서 실행될 소프트웨어 코드를 보내는 것이라 할 수 있습니다.

Java applets와 javaScript등이 있습니다. 

REST API

위와 같이 REST의 조건들을 지켜 API를 개발하였을 경우 이를 REST API라고 하며 이러한 REST API들로 개발된 웹 서비스를 'RESTful 하다' 라고 표현할 수 있습니다.

 

여러가지 장단점이 있는데, 

 

REST가 표준이 존재하지 않고 아키텍처 스타일이다보니 발생하는 장단점이 크다고 생각합니다.

장점은 위에서 계속 나왔듯이 확장성과 자율성이 뛰어난 API를 구축할 수 있다로 볼 수 있고,

단점은 너무 자율성이 높고 조건들이 많다보니 지켜지기 쉽지 않다고 볼 수 있습니다.

 

마치며

개인적으로 1, 2, 3, 5번은 HTTP API를 개발하면서 크게 신경쓰지 않아도 지켜지는 조건들이라고 생각합니다.

다만 4번의 조건들이 신경쓸 부분들이 많은 만큼 만약 REST API를 만들고 싶다면 조건들을 세심히 따져보시기 바랍니다.

 

REST API는 표준이 정해져 있지 않지만 조건들이 존재하는 만큼 어느정도 커스텀은 괜찮다고 보지만, 조건이 맞지 않다면 그냥 API개발했구나 생각하시면 될 것 같습니다. ( 제가 그랬습니다 ㅎㅎ )  


 참조

https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm

https://blog.npcode.com/2017/04/03/rest%EC%9D%98-representation%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80/

728x90
728x90

JPA에 대하여

 

우리가 JPA를 알기 위해서는 사전 지식으로 ORM이 무엇인지를 알아야 합니다.

이에 이 글은 ORM부터 알아본 후 JPA에 대해 다뤄보도록 하겠습니다.

ORM이란?

ORM( Object Ralational Mapping )은 단어를 풀어 해석하면 '객체 관계형 연결'이 됩니다.

이 기술은 애플리케이션과 데이터베이스 연결 시 기존에는 SQL언어를 애플리케이션 서버에서 직접 작성하였지만, 이를 서버에서는 객체로 정의하여 행위에 대한 Action을 하면  정의된 객체를 해석하여 행위에 필요한 SQL문을 작성하여 데이터베이스로 전달하는 말 그대로의 Mapping역할을 합니다.

 

이러한 ORM은 기존 Mybatis와 같은 기술을 사용하던 것을 '객체 지향'적으로 사용하기 위해 나온 기술이라고 봐도 무방 할 것이라 생각하는데, 어떤 탄생 배경이 있는지 알아보도록 하겠습니다.

ORM 탄생 배경

우리가 개발할 때 객체 지향 언어를 사용하여 개발을 하고, 관계형 데이터베이스를 사용하게 되면 프로그램 내에서 데이터베이스와 연결하여 SQL문을 직접 작성을 해줘 데이터를 얻거나 작업을 할 수 있습니다.

 

이때 우리는 객체 지향 언어의 데이터 표현 방법관계형 데이터베이스의 데이터 표현 방법의 차이를 확인해 볼 필요가 있습니다.

 

객체 지향 언어은 데이터를 객체에 상호 연결된 그래프로 표현을 하고, 관계형 데이터베이스는 데이터를 표형식으로 표현을 하게 됩니다.

이렇게 표현 방식이 다른 두 개념을 한 프로그램에서 사용을 하려면 표형식 표현방법을 객체에 상호 연결된 그래프 방식으로 변경하는 작업을 하게 되는데, 이는 개발자에게 번거로움과 오류 발생의 원인을 제공하게 됩니다.

 

이리하여 관계형 데이터베이스와 연결하여 SQL문을 작성하는 부분을 추상화시켜버리게 되는데 이것이 ORM입니다.

ORM의 장단점

위 설명만 보면 ORM을 만능으로 볼 수도 있습니다.

개발자의 번거로움을 줄여주고!! 오류 발생의 원인을 없애 주다니!! 하고 말입니다.

하지만 대부분의 기술이 그렇듯 장단점이 존재합니다.

장점

  • 사용하기 편하다.  - 강점 -
  • 직접적인 SQL문 작성이 없는 만큼 신경써야하는 부분이 줄어든다.
  • 객체를 정의하여 사용하고 얼마든지 수정 및 확장이 쉬워 유지보수가 편하다.

단점

  • ORM만 사용하여서는 복잡한 작업( 조회 혹은 수정 등 )을 대응하기 힘들다.
  • 객체 지향적이라 객체로 관리하게 되었다고 설계를 신경써서 하지 않으면 어떤 프로그램보다 복잡하고 힘든 프로그램이 만들어진다.

데이터 베이스를 객체 지향적으로 접근할 수 있는 만큼 설계의 중요성은 이루 말할 수 없을 정도이며,

사용하다 보면 그냥 SQL사용해서 개발하고 싶다라는 생각이 들 수도 있습니다.

다만 적절히 비즈니스 로직을 나눠 분석하여 사용할 경우보다 쉽게 개발할 수 있는 기술인 것에는 틀림없다 생각합니다.


JPA이란?

JPA(Java Persistent API)는 말그대로 JAVA Application에서 '객체와 관계형 데이터베이스 간의 매핑을 위한 API' 인터페이스의 모음입니다.

이 모음에는 3가지 구현체가 존재하는데, Hibernate, EclipseLink, DataNucleus가 있습니다.

Hibernate

하이버네이트는 JDBC API를 사용하는 JPA인터페이스 구현체입니다.

이는 무슨 뜻이냐 우리가 객체로 데이터를 CRUD 하게 되면 JPA 즉 하이버네이트 내부에서는 해당 객체를 분석하여 데이터베이스로 SQL문을 보내주게 되는데 이때 사용하는 API가 JDBC API라는 것입니다.

EclipseLink

EclipseLink는 JPA인터페이스 구현체입니다.

또한 JAXB, SDO를 구현한 포괄적인 오픈소스 프레임워크로, 다양한 기능과 확장성을 강력하게 제공하고 있습니다.

DataNucleus

DataNucleus는 JPA 인터페이스 구현체입니다.

EclipseLink와 마찬가지로 다른 데이터베이스들 또한 지원하고 있습니다.

Hibernate를 왜 많이 사용하는가

글을 작성하면서 의문이 들었던 점이 왜 Hibernate는 많이 사용하거나 본 거 같은데, EclipseLink와 DataNucleus는 다루는 글을 많이 못 본 것 같다는 생각을 하였습니다. 그리고 설명들을 보면 Hibernate는 JPA 인터페이스를 구현한 가장 대표적인 오픈소스라고 하는데 EclipseLink와 DataNucleus 또한 강력한 기능과 확장 가능성을 지원해 준 것을 확인할 수 있었습니다.

 

우선 찾아보며 생각한 것은 Hibernate의 강력한 커뮤니티와 그로 인해 나온 많은 선례, 또한 만들어진 지 오래된 만큼 안정성을 보장하기 때문이라 생각합니다.

JPA주요 특성

ORM

엔티티 클래스와 데이터베이스 테이블 간의 매핑을 지원합니다.

영속성 컨텍스트

데이터를 영속성 컨텍스트에 저장하고 지속적으로 추적하게 되는데, 이 데이터가 수정될 경우 트랜젝션이 걸려있지 않으면 바로 반영이 됩니다. 이게 무슨 의미인가 만약 데이터를 조회하여 엔티티로 받아서 바로 엔티티 속성을 수정한다면 실제 SQL문이 바로 날아가 변경된다는 뜻입니다.

 

즉 commit을 안 했는데 commit이 되는 현상으로 이러한 상황을 방지하기 위해서는 Transaction처리를 해주거나, 엔티티를 사용하지 말고 데이터를 수정할 때 사용하는 객체를 따로 만들어 해당 객체로 데이터 값들만 복사하여 사용하는 것이 안전한 사용법인 것 같습니다.

캐싱

영속성 컨텍스트는 한번 조회된 데이터들은 가지고있는데, 만약 조회되는 데이터가 영속성 컨텍스트에 저장되어있는 데이터라면 데이터베이스를 조회하지 않고 해당 데이터를 반환해줍니다. 이는 1차 캐시의 개념이며, 만약 1차 캐시에 없을 경우 2차 캐시를 조회하게되고, 거기에도 없다면 데이터베이스를 조회하게 됩니다.

 

이때 말하는 1차 캐시는 Hibernate에서 제공하는 영속성 컨텍스트 캐시이고, 2차 캐시는 Session Factory 캐시입니다.


Reference

 

Understanding EclipseLink

This chapter describes how to set up your JPA applications to work with a non-relational data source. There are many types of non-relational data sources. These include document databases, key-value stores, and various other non-standard databases, such as

eclipse.dev

 

 

JPA Getting Started Guide (v5.2)

Developing applications is, in general, a complicated task, involving many components. Developing all of these components can be very time consuming. The Java Persistence API (JPA) was designed to alleviate some of this time spent, providing an API to allo

www.datanucleus.org

 

 

What is Object/Relational Mapping? - Hibernate ORM

Idiomatic persistence for Java and relational databases.

hibernate.org

 

캐싱 참조자료: https://www.baeldung.com/hibernate-second-level-cache

 

728x90

+ Recent posts