Red Glitter Pointer

 

클라이언트와 서버가 통신할 때 HTTP 통신을 주로 사용한다.

HTTP 통신은 비연결성(connectionless), 무상태성(stateless), 단방향 통신이라는 특징을 가지고 있는데 이러한 HTTP 통신의 경우 채팅과 같은 실시간 통신에는 적합하지 않다.

물론 흉내는 낼 수 있으나 완벽하지는 않다. 실시간 통신이 필요한 경우 사용하는 통신을 소켓 통신 이라고 한다.

HTTP 통신과 다르게 연결을 맺고 바로 끊어버리는 것이 아니라 계속 유지하기 때문에 실시간 통신에 적합하다

 

 

 

 

 1. STOMP (Simple Text Oriented Messaging Protocol)

 

STOMP란? 

메시징 전송을 효율적으로 하기 위한 프로토콜로 PUB / SUB 기반으로 동작한다.

메시지의 발행자와 구독자가 존재하고, 메시지를 보내는 사람과 받는 사람이 구분되어 있다.

메시지 브로커는 발행자가 보낸 메시지를 구독자에게 전달해주는 역할을 한다.

 

 

STOMP 형식 

STOMP는 HTTP와 비슷하게 frame 기반 프로토콜 command, header, body로 이루어져 있다.

COMMAND
header1:value1
header2:value2

Body^@

 

  • 클라이언트는 메시지를 전송하기 위해 COMMAND로 SEND 또는 SUBSCRIBE 명령을 사용하며 header와 value로 메시지의 수신 대상과 메시지에 대한 정보를 설명할 수 있다.
  • 일반적으로는 아래의 형식을 따른다
"topic/.." -> publish-subscribe (1:N)
"queue/" -> point-to-point (1:1)


// ex) 클라이언트 A가 5번 채팅방 구독
SUBSCRIBE
destination: /topic/chat/room/5
id: sub-1

^@


// ex) 클라이언트B가 채팅 메시지 전송
SEND
destination: /pub/chat
content-type: application/json

{"chatRoomId": 5, "type": "MESSAGE", "writer": "clientB"} ^@

 

  • STOMP 서버는 모든 구독자에게 메시지를 브로드캐스팅(BroadCasting)하기 위해 MESSAGE COMMAND를 사용할 수 있다.
  • 서버는 내용을 기반으로 메시지를 전송할 브로커에게 전달한다.
MESSAGE
destination: /topic/chat/room/5
message-id: d4c0d7f6-1
subscription: sub-1

{"chatRoomId": 5, "type": "MESSAGE", "writer": "clientB"} ^@

 

  • 서버는 불분명한 메시지를 전송할 수 없기 때문에 서버의 모든 메시지는 특정 클라이언트 구독에 응답해야 하고, 서버 메시지의 "subscription-id" 헤더는 클라이언트 구독 id 헤더와 일치해야 한다.

 

 

 

STOMP의 장점 

WebSocket만 사용해서 구현하면 해당 메시지가 어떤 요청인지, 어떤 포맷인지, 메시지 통신 과정을 어떻게 처리해야 하는지 정해져 있지 않아 일일이 구현해야 한다.

 

STOMP를 서브 프로토콜로 사용하면, 클라이언트와 서버가 서로 통신할 때 메시지의 형식, 유형, 내용 등을 정의해준다. 따라서 메시지 송수신에 대한 처리를 명확하게 정의할 수 있고 WebSocketHandler를 직접 구현할 필요 없이, 어노테이션을 사용하여 메시지 발행 시 엔드포인트를 별도로 분리하여 관리할 수 있다.

 

 

 

 

 2. Spring WebSocket STOMP

 

 

Getting Started | Using WebSocket to build an interactive web application

In Spring’s approach to working with STOMP messaging, STOMP messages can be routed to @Controller classes. For example, the GreetingController (from src/main/java/com/example/messagingstompwebsocket/GreetingController.java) is mapped to handle messages t

spring.io

 

의존성 주입 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

 

 

 

WebSocket STOMP 설정 

WebSocketConfig.java
package com.example.messagingstompwebsocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker // ①
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) { // ④
    config.enableSimpleBroker("/topic"); // ⑤
    config.setApplicationDestinationPrefixes("/app"); // ⑥
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) { // ②
    registry.addEndpoint("/chattings"); // ③
  }

}

 

① 메시지 브로커가 지원하는 WebSocket 메시지 처리를 활성화

② HandShake와 통신을 담당할 EndPoint 지정

③ WebSocket 연결 시 요청을 보낼 endPoint 지정

④ 메모리 기반의 Simple Message Broker 활성화

⑤ 메시지 브로커가 Subscriber들에게 메시지를 전달할 URL 지정(메시지 구독 요청) - 발행자가 "/subscription" 의 경로로 메시지를 주면 구독자들에게 전달.

⑥ 클라이언트가 서버로 메시지 보낼 URL 접두사 지정(메시지 발행 요청) - 발행자가 "/publication" 의 경로로 메시지를 주면 가공해서 구독자들에게 전달. 

 

 

 

메시지 브로커

① 구독자는 특정 topic을 구독하고 발행자는 해당 topic으로 메시지를 날린다

② 메시지 브로커는 이 메시지를 관리하여 이를 구독 중인 구독자들에게 메시지를 보내준다.

③ 메시지를 바로 전달하는 것이 아니라 중간에 존재하는 메시지 브로커에게 전달하고 이 브로커가 구독자들에게 전달해주는 형태이다.

 

 

 

enableSimpleBroker

스프링 document에서 설명을 보면

Enable a simple message broker and configure one or more prefixes to filter destinations targeting the broker (e.g.

 

1. simple message broker을 활성화한다.

2. 브로커를 타겟으로하는 하나 이상의 접두사를 구성한다.

 

"/topic" 접두사가 붙은 경로는 브로커를 타겟으로 한다는 설정을 한 것이다.

 

🌟 즉, "/topic/..." 경로로 메시지를 보내면 이 메시지는 브로커를 향하게 되고, 브로커는 이 경로를 구독중인 구독자들에게 메시지를 발송한다.

 

 

 

 

setApplicationDestinationPrefixes

스프링 document에서 설명을 보면

Configure one or more prefixes to filter destinations targeting application annotated methods.

 

1. application annotated method를 타겟으로 하는 하나 이상의 접두사를 구성한다.

 

"/app" 접두사가 붙은 경로는 @MessageMapping 어노테이션이 붙은 곳을 타겟으로 한다는 설정을 한 것이다.

 

🌟 "/app/..." 경로로 메시지를 보내면 이 메시지는 @MessageMapping 어노테이션이 붙은 곳으로 향하게 된다.

 

@MessageMapping으로 이동하게 되면 그 곳에서 이 메시지를 가공할 수 있게 된다.

또한 @SendTo 어노테이션을 사용하여 메시지가 향할 곳을 정할 수 있다.

 

 

 

 

ChattingController 작성 

ChattingController.java
import com.jeongyuneo.springwebsocket.dto.ChattingRequest;
import com.jeongyuneo.springwebsocket.service.ChattingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

@Slf4j
@RequiredArgsConstructor
@Controller
public class ChattingController {

    private final SimpMessagingTemplate simpMessagingTemplate;  // ①

    @MessageMapping("/chattings/{chattingRoomId}/messages")  // ②
    public void chat(@DestinationVariable Long chattingRoomId, ChattingRequest chattingRequest) {  // ③
        simpMessagingTemplate.convertAndSend("/subscription/chattings/" + chattingRoomId, chattingRequest.getContent());
        log.info("Message [{}] send by member: {} to chatting room: {}", chattingRequest.getContent(), chattingRequest.getSenderId(), chattingRoomId);
    }
}

 

① @EnableWebSocketMessageBroker를 통해 등록되는 Bean으로, Broker로 메시지 전달

② 클라이언트가 SEND할 수 있는 경로

WebSocketConfig에서 등록한 applicationDestinationPrefixes와 @MessageMapping의 경로가 합쳐진다.

③ 클라이언트에서 /publication/chattings/{chattingRoomId}/messages 로 메시지를 보내면 해당 채팅방을 구독 중인 사용자들에게 메시지를 전달

@DestinationVariable은 구독 및 발행 URL의 경로 변수를 지정한다.

 

 

ChattingRequest.java
import lombok.*;

@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChattingRequest {

    private Long senderId;
    private String content;
}

 

 

 

📌 메시지를 전송하는 과정 정리

 

① "/topic"으로 시작하는 경로는 메시지 브로커를 향하도록 설정한다.

② "/app"으로 시작하는 경로는 @MessageMapping을 향하도록 설정한다.

③ 구독자는 "/topic/greetings"으로 시작하는 경로를 구독한다. (1번 설정 때문에)

④ 발행자는 "/app/hello"로 시작하는 경로로 메시지를 보낸다.

⑤ 2번 설정 때문에 @MessageMapping("/hello")가 붙어있는 곳으로 메시지가 간다.

⑥ 메시지 가공이 끝난 뒤 ("/topic/greetings") 로 보낸다.

⑦ 1번 설정 때문에 이 메시지는 메시지 브로커로 가게 된다.

⑧ 메시지 브로커에서 "/topic/greetings"를 구독 중인 구독자들에게 메시지를 전송한다.

 

 

 

 

 

 

 

 

 

🌐 참고 링크

https://eoneunal.tistory.com/17

https://growth-coder.tistory.com/157

 

 

 

값 타입

int, String, Integer 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체.

 

 

 

값 타입 분류

  • 기본값 타입
    • 자바 기본 타입(int, double)
    • 래퍼 클래스(Integer, Long)
    • String
  • 임베디드 타입(Embedded type, 복합 값 타입)
  • 컬렉션 값 타입(Collection value type)

 

 

 

값 타입 컬렉션

값 타입을 컬렉션에 담아서 사용하는 것을 값 타입 컬렉션이라고 한다!

 

//기본 값 타입 컬렉션
List<String> stringList = new ArrayList()<>;

//임베디드 값 타입 컬렉션
Set<Address> addressSet = new HashSet<>();

 

위와 같이 데이터베이스 안에 값 타입 컬렉션을 저장하고 싶을 땐 어떻게 해야할까?

 

 

 

 

 

 

⚠️ 기본적으로 관계형 데이터베이스에는 컬렉션을 저장할 수 없다!

 

따라서 컬렉션을 저장하기 위해서는 별도의 테이블을 만들어서 컬렉션을 저장해야 함.

이때 사용하는 것이 @ElementCollection과 @CollectionTable 이다!

 

 

 

 

 

@ElementCollection

JPA가 컬렉션 객체임을 알 수 있게 한다.

엔티티가 아닌 값 타입, 임베디드 타입에 대한 테이블을 생성하고 1:N 관계로 다룬다.

 

 

 

@CollectionTable

값 타입 컬렉션을 매핑할 테이블에 대한 역할을 지정하는 데 사용한다

테이블의 이름과 조인정보를 적어줘야 함.

 

 

 

 

값타입 컬렉션의 제약사항

  • 값 타입은 Entity와 다르게 식별자 개념이 없다
  • 값은 변경하면 추적이 어렵다
  • 값타입 컬렉션에 변경 사항이 발생하면, 주인 Entity와 연관된 모든 데이터를 삭제하고, 값타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야함. Not Null, 중복 저장 안됨

 

 

🏷️ 복잡하다면 다르게 풀어야 한다. (값 타입 컬렉션 대안)

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신 일대다 관계를 고려한다.
  • 일대다 관계를 위한 Entity를 만들고, 여기에서 값 타입을 사용한다.
  • 영속성 전이(CASCADE) + 고아 객체 제거를 사용하여 값 타입 컬렉션처럼 사용한다! 

 

 

 

 

그렇다면 값타입은 언제 쓸까?

 

 

진짜 단순할 때,,, 치킨 피자를 select box로 만들어서 사용할 때..

 

 

 

 

정리

  • Entity 타입의 특징
    • 식별자 O (ID)
    • 생명주기 관리
    • 공유
  • 값 타입의 특징
    • 식별자 X (ID)
    • 생명주기를 Entity에 의존
    • 공유하지 않는 것이 안전하다 (복사하여 사용)
    • 불변 객체로 만드는 것이 안전하다
  • Entity와 값타입을 혼동해서 Entity를 값타입으로 만들면 안됨 !!! ⚠️
  • 식별자가 필요하고, 지속해서 값을 추적 및 변경해야 한다면 값타입이 아닌 Entity

 

 

 

 

 

스프링 시큐리티를 사용하면
인증 / 인가를 편리하게 구현할 수 있다.

 

 

 

스프링 시큐리티란?

스프링 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크! 

 

 

 

보안 언어 정리

이름 설명
인증(Authentication) 접근하려는 유저가 누구인지 확인하는 절차
ex) 회원가입하고 로그인
인가(Authorization) '권한 부여'나 '허가'와 같은 의미로 사용된다. 인증된 사용자에 대해 권한을 확인하고 허락하는 것
접근 주체(Principal) 보호된 대상에 접근하는 유저
비밀번호(Credential) 대상에 접근하는 유저의 비밀번호
권한(Role) 인증된 주체가 어플리케이션의 동작을 수행할 수 있도록 허락되었는지를 결정할 때 사용

 

✔️ 사용자가 게시글을 작성하기 위해 로그인을 했다 ? 👉 인증

✔️ 다른 사람이 작성한 게시글의 수정하기 버튼을 눌렀지만 수정할 수 없었다 👉 인가

 

사이트에 대해 유효한 사용자인지 확인하는 것이 인증이고, 인증된 사용자가 사용할 수 있는 기능인지 확인하는 것이 인가라고 생각하면 될 것 같다.! 그렇기 때문에 인증이 먼저 이루어지고, 인가가 이루어져야 한다.

 

 

 

 

Credential 기반의 인증 방식

Spring Security 에서는 이러한 인증, 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용한다.

 

 

 

 

 

서블릿 필터

 

스프링 시큐리티는 필터(Filter) 기반으로 동작하기 때문에 스프링 MVC와 분리되어 관리 및 동작한다.

사용자 요청이 서블릿에 전달되기 전, 스프링 시큐리티는 필터의 생명주기를 이용하여 인증과 권한 작업을 수행한다. 하지만 서블릿 컨테이너는 스프링 컨테이너에 등록된 빈을 인식할 수 없다.

 

 

 

📌 FilterChainProxy

DelegatingFilterProxy 를 통해 받은 요청과 응답을 스프링 시큐리티 필터체인에 전달하고 작업을 위임하는 역할

 

🤔 DelegatingFilterProxy 에서 바로 SecurityFilterChain 을 실행시킬 수 있지만 중간에 FilterChainProxy 를 둔 이유 ?

서블릿을 지원하는 시작점 역할을 하기 위함! 이를 통해 서블릿에서 문제가 발생할 경우, FilterChainProxy의 문제라는 것을 알 수 있다. 또한 FilterChainProxy에서 어떤 체인에게 작업을 위임할지도 결정할 수 있다.

 

 

 

📌 SecurityFilterChain

인증을 처리하는 여러 개의 시큐리티 필터를 담는 필터 체인이다.

여러 개의 SecurityFilterChain을 구성하여 매칭되는 URL에 따라 다른 SecurityFilterChain이 사용되도록 할 수 있다.

 

 

 

📌 SecurityFilters

요청을 스프링 시큐리티 매커니즘에 따라 처리하는 필터!

SecurityFilters에는 순서가 존재한다.

 

 

 

 

 

 

아키텍처

Username과 Password 방식의 아키텍처는 아래와 같다.

스프링 시큐리티는 기본적으로 세션-쿠키 방식으로 인증한다.

 

 

  1. 유저가 로그인 요청 (Http Request)
  2. AuthenticationFilter 에서 UsernamePasswordAuthentication Token 을 생성하여 AuthenticationManager 에 전달
  3. AuthenticationManager 은 등록된 AuthenticationProvider 들을 조회하여 인증 요구
  4. AuthenticationProvider 은 UserDetailService 를 통해 입력받은 아이디에 대한 사용자 정보를 User(DB) 에서 조회
  5. User 에 로그인 요청한 정보가 있는 경우 UserDetails 로 꺼내서 유저 session 생성
  6. 인증이 성공된 UsernameAuthenticationToken 을 생성하여 AuthenticationManager 로 반환
  7. AuthenticationManager 은 UsernameAuthenticationToken 을 AuthenticationFilter 로 전달
  8. AuthenticationFilter 은 전달받은 UsernameAuthentication 을 LoginSuccessHandler 로 전송하고, spring security 인메모리 세션저장소인 SecurityContextHolder 에 저장
  9. 유저에게 session ID 와 응답을 내려줌

 

 

📌 AuthenticationFilter

  • 스프링 시큐리티는 연결된 필터를 가지고 있음
  • 모든 Request는 인증과 인가를 위해 해당 필터를 통과
  • SecurityContext에 사용자의 세션ID가 있는지 확인하고 세션ID가 없는 경우 다음 로직 수행
  • 인증에 성공하는 경우, 인증된 Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
  • 인증 실패하는 경우, AuthenticationFailureHandler 실행

 

 

📌 UsernamePasswordAuthenticationToken

  • Authentication을 구현한 AbstractAuthentication Token의 하위 클래스
  • principal 👉 username / credentials 👉 password
  • UsernamePasswordAuthenticationToken(Object principal, Object credentials) : 인정 전의 객체를 생성
  • UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) : 인증 완료된 객체를 생성 

 

 

📌 AuthenticationManager

  • Authentication을 만들고 인증을 처리하는 interface
  • 로그인시 인자로 받은 Authentication을 Provider를 통해 유효한지 처리하여 Authentication 객체를 리턴한다

 

 

📌 ProviderManager

  • AuthenticatoinManager의 구현체
  • 사용자 요청을 인증에 필요한 AuthenticatoinProvider를 살펴보고 전달된 인증 객체를 기반으로 사용자 인증 시도

 

 

📌 AuthenticationProvider

  • 실제 인증을 담당하는 인터페이스
  • 인증 전 Authentication 객체를 받아서 DB에 있는 사용자 정보를 비교하고 인증된 객체를 반환

 

 

📌 UserDetailsService

  • DB에서 유저 정보를 가져오는 역할
  • loadUserByUsername() 메소드를 통해서 DB에서 유저 정보를 가졍돈다.
  • 커스텀하게 사용하고 싶다면 해당 interfacee를 implements 받아서 loadUserByUsername() 메소드를 구현하면 됨

 

 

📌 UserDetails

  • 사용자의 정보를 담는 인터페이스
메소드 설명
getAuthorities() 계정의 권한 목록을 리턴
getPassword() 계정의 비밀번호 리턴
getUsername() 계정의 고유한 값 리턴
isAccountNonExpired() 계정의 만료 여부 리턴
isAccountNonLocked() 계정의 잠김 여부 리턴
isCredentialsNonExpired() 비밀번호 만료 여부 리턴
isEnabled() 계정의 활성화 여부 리턴

 

 

📌 SecurityContextHolder

 

  • SecurityContext를 현재 스레드와 연결 시켜주는 역할
  • 스프링 시큐리티는 같은 스레드의 어플리케이션 내 어디서든 SecurityContextHolder의 인증 정보를 확인 가능하도록 구현되어 있는데 이 개념을 ThreadLocal 이라고 한다.

 

 

📌 SecurityContext

  • Authentication의 정보를 가지고 있는 interface
  • SecurityContextHolder.getContext()를 통해 얻을 수 있다

 

 

📌 Authentication

  • 현재 접근하는 주체의 정보와 권한을 담는 인터페이스
  • AuthenticationManager.authenticate(Authentication)에 의해 인증된 principal 혹은 token
 이름 설명
principal 사용자 정보
authorities 사용자에게 부여된 권한
ex) ROLE_ADMIN
credentials 자격 증명

 

 

 

 

 

 

 

Controller에서 사용하는 @RequestParam 어노테이션과 @PathVariable의 차이에 대해 작성해볼 예정이다!

 

 

 

@RequestParam과 @PathVariable의 공통점

Request URI를 통해 전달된 값을 파라미터로 받아오는 역할을 한다.

 

 

 

 

@RequestParam과 @PathVariable의 차이점

값을 얻는 방식에서 차이가 있다! 

 

 

URI를 통해 값을 전달하는 방식은 아래와 같다.

 

1. http://localhost:8080/board?id=1

2. http://localhost:8080/board/1

 

 

  • 위 예시에서 첫번째와 같은 방식은, 쿼리스트링을 사용하여 여러 개의 값을 전달받기 때문에 @RequestParam을 통해 받아와야 한다.
  • 두번째 방식은 @PathVariable을 사용하여 받아올 수 있다.

 

 

 

@RequestParam

  • Query String으로부터 값을 얻는다. 🌟key = value 형태임
  • http://localhost:8080/board?id=1
@GetMapping("/board")
public ResponseEntity<ReadResponse> read(@RequestParam Long id) {
    BoardResponse response = BoardAssembler
        .readResponse(boardService.read(BoardAssembler.readRequestDto(id)));
    
    return ResponseEntity.ok(response);
}
  • get요청을 받으면 쿼리스트링을 통해 전달된 id값을 받아와서 @RequestParam이 파라미터인 Long id에 대입해준다.

@RequestParam의 4가지 파라미터

 

defaultValue : 값을 설정하지 않았을 때 기본값

name : 바인딩할 파라미터의 이름

value : name의 별칭

required : 필수 값 사용 여부 설정 (무조건 설정 해주어야 함.)

 

 

 

 

 

@PathVariable

  • URI path로부터 값을 얻는다
  • 어떤 요청이든 하나만 사용 가능! 🌟
  • @RequestParam과는 다르게 default 값을 설정하지 않는다.
  • http://localhost:8080/board/1
@GetMapping("/board/{id}")
public ResponseEntity<ReadResponse> read(@PathVariable Long id) {
    ReadResponse response = BoardAssembler
        .readResponse(boardService.read(BoardAssembler.readRequestDto(id)));

    return ResponseEntity.ok(response);
}

@PathVariable 의 3가지 파라미터

 

name : 바인딩할 파라미터의 이름

value : name의 별칭

required : 필수 값 사용 여부 설정 (무조건 설정 해주어야 함.)

 

 

 

 

 

정리 및 요약

  • @RequestParam과 @PathVariable은 둘 다 데이터를 받아오는 데 사용된다
  • @PathVariable은 값을 하나만 받아올 수 있으므로, 쿼리스트링을 이용하여 여러 개의 데이터를 받아올 땐 @RequestParam을 사용한다
  • default 값을 설정하고 싶을 땐 @RequestParam을 사용하면 된다.

 

 

 @Size

  • Java Bean Validation 어노테이션
  • 필드 크기가 min과 max 사이여야 값을 저장할 수 있도록 유효성 검사를 해줌
  • JPA나 Hibernate로부터 독립적인 bean을 만들어줌
public class User {

    @Size(min = 4, max = 10)
    private String username;  // 4 ~ 10 범위 밖의 크기 값이 들어오면 Exception 발생

}

 

 

 

 @Length

  • Hibernate 어노테이션
  • min과 max를 이용하여 필드 값 크기에 대한 유효성 검사
public class User {

    @Length(min = 4, max = 10)
    private String username;

}

 

 

 

 @Column(length = value)

  • JPA에서 제공하는 어노테이션
  • JPA가 @Entity가 붙은 클래스를 DB테이블로 생성할 때, @Column의 length 속성을 사용하여 컬럼 문자열의 길이를 정한다.
    • DDL을 컨트롤하기 위해 사용된다.
  • length 보다 긴 문자열을 넣으려고 하면 SQL error 발생
  • 유효성 검사를 해주는 것이 아니라 테이블 컬럼의 길이 속성만 지정해주는 것
  • String 필드 경우에만 적용되며, length 값을 지정해주지 않는다면 기본값으로 varchar(255) 크기로 생성됨
@Entity
public class User {

		@Column(length = 20)
		private String password;

}

 

 

 

💡 @Size 를 사용할 경우 장점!

1. @Column 의 예시와 같이 DDL statement가 동일하게 varchar(20) 이 된다.
2. Hibernate의 validation bean이 persist, update 전에 자동으로 @Size에 해당하는 값에 맞게 데이터가 할당되었는지 검증한다. 
3. @Length 보다 @Size가 더 가볍다고 한다

 

 

 

 

 

 DB 및 logging 설정 추가

application.yml 에 설정 추가 (또는 application.properties)

spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/DB명?characterEncoding=UTF-8&serverTimezone=UTC
    username: userName
    password: password

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true # 라인 포맷에 맞춰서 보기 좋게 예쁘게 출력
        highlight_sql: true # ANSI 코드를 사용하여 색감 부여
        use_sql_comments: true # SQL 내부에 주석 추가

logging:
  level:
    org.hibernate:
      type.descriptor.sql: trace # 쿼리문 로그에 출력되어 있는 파라미터에 바인딩 되는 값 확인
      SQL: DEBUG # logger를 이용하여 실행되는 모든 쿼리문 출력

 

 

 DAO (Data Access Object) 란?

repository package

  • repository와 거의 유사함
    • 좀 더 깊이있게 차이를 설명하자면, repository는 엔티티 객체를 보관하고 관리하는 저장소이고, DAO는 데이터에 접근하도록 DB 접근 관련 로직을 모아둔 객체이다. 개념 차이일뿐 실제 개발할 때는 비슷하게 사용한다.
  • DB의 데이터에 접근하기 위한 객체
    • DB에 접근하기 위한 로직을 분리하기 위해 사용한다
    • 직접 DB에 접근하여 data를 삽입, 삭제, 조회 등 조작할 수 있는 기능을 수행한다
  • Persistence Layer(DB에 data를 CRUD하는 계층)
  • Service와 DB를 연결하는 역할
  • SQL 사용(개발자가 직접 코딩)하여 DB에 접근한 후 적절한 CRUD API 제공
    • JPA 대부분의 기본적인 CRUD method를 제공하고 있따
    • extends JpaRepository<User, Long>
public interface ChattingLogRepository extends MongoRepository<ChattingLog, String> {
	
    public ChattingLog findAllByRoomIdx(Long roomIdx);
    
    public List<ChattingLog> findByRoomIdx(Long roomIdx);
}

 

 

 

 

 DTO(Data Transfer Object) 란?

dto package

  • 계층 간 데이터 교환을 위한 객체(Java Beans)
  • DB에서 데이터를 얻어 Service나 Controller 등으로 보낼 때 사용하는 객체를 말한다
  • DB의 데이터가 Presentation Logic Tier로 넘어오게 될 때는 DTO의 모습으로 바뀌어 오고가는 것임
  • 순수한 데이터 객체이다.
    • DTO는 목적 자체가 로직을 가지고 있지 않고 단순히 데이터를 전달하는 것
  • getter / setter 메소드만을 갖는다. 
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
  @NotBlank
  @Pattern(regexp = "^([\\w-]+(?:\\.[\\w-]+)*)@((?:[\\w-]+\\.)*\\w[\\w-]{0,66})\\.([a-z]{2,6}(?:\\.[a-z]{2})?)$")
  private String email;

  @JsonIgnore
  @NotBlank
  @Size(min = 4, max = 15)
  private String password;

  @NotBlank
  @Size(min = 6, max = 10)
  private String name;

  public User toEntity() {
      return new User(email, password, name);
  }

  public User toEntityWithPasswordEncode(PasswordEncoder bCryptPasswordEncoder) {
      return new User(email, bCryptPasswordEncoder.encode(password), name);
  }
}


// 출처
// https://gmlwjd9405.github.io/2018/12/25/difference-dao-dto-entity.html

 

 

 

 

 VO(Value Object) 란?

  • Read-Only 속성을 가진 값 오브젝트 이다.
  • 자바에서 단순히 값 타입을 표현하기 위해 불변 객체를 만들어 사용한다.
  • getter 기능만 존재함!
  • VO의 핵심 역할은 equals()와 hashcode()를 overriding 하는 것
    • equals, hashCode Method를 구현하여 중요한 Data를 전달할 때는 VO를 생성하여 객체 비교까지 필요한 로직 내에서 주로 사용한다. 
@Getter
public enum BaseResponseStatus {
    SUCCESS(true, 1000, "요청에 성공하였습니다."),

    REQUEST_ERROR(false, 2000, "입력값을 확인해주세요."),
    RESPONSE_ERROR(false, 3000, "값을 불러오는데 실패하였습니다."),
    DATABASE_ERROR(false, 4000, "데이터베이스 연결에 실패하였습니다.");
    
    private final boolean isSuccess;
    private final int code;
    private final String message;

    private BaseResponseStatus(boolean isSuccess, int code, String message) {
        this.isSuccess = isSuccess;
        this.code = code;
        this.message = message;
    }
}

✔️ 위 코드는 BaseResponseStatus라는 enum으로 요청의 상태를 저장한 enum이다. 즉, 값은 고정되어 있고 불변하는 클래스이다. 

 

💡 참고

VO는 DTO와 동일한 개념이지만, VO는 특정한 비즈니스 값을 담는 객체이고, DTO는 Layer간의 통신 용도로 오고가는 객체를 말한다.

 

 

 

 

 Entity Class 란?

domain package

  • 실제 DB의 테이블과 매칭될 클래스
    • 테이블과 링크될 클래스임을 나타낸다
    • Entity 클래스 또는 가장 Core한 클래스라고 부른다
    • @Entity, @Column, @Id 등을 이용한다
  • 최대한 외부에서 Entity 클래스의 getter method를 사용하지 않도록 해당 클래스 안에서 필요한 로직 method를 구현한다
    • 단, Domain Logic만 가지고 있어야 하고 Presentation Logic을 가지고 있어서는 안된다
    • 여기서 구현한 method는 주로 Service Layer에서 사용한다
💡 Entity 클래스와 DTO 클래스를 분리하는 이유

1. View Layer와 DB Layer의 역할을 철저하게 분리하기 위함
2. 테이블과 매핑되는 Entity 클래스가 변경되면 여러 클래스에 영향을 끼치게 되는 반면, View와 통신하는 DTO 클래스(Request / Response 클래스)는 자주 변경되므로 분리해야 한다.
3. Domain Model을 아무리 잘 설계했다고 하더라도 각 View 내에서 Domain Model의 getter만을 이용하여 원하는 정보를 표시하기가 어려운 경우가 있음. 이런 경우 Domain Model 내에 Presentation을 위한 필드나 로직을 추가하게 되는데, 이러한 방식이 모델링의 순수성을 깨고 Domain Model 객체를 망가뜨리게 된다.
4. Domain Model을 복잡하게 조합한 형태의 Presentation 요구사항들이 있기 때문에 Domain Model을 직접 사용하는 것은 어렵다. 
5. 즉 DTO는 Domain Model을 복사한 형태로, 다양한 Presentation Logic을 추가한 정도로 사용하며 Domain Model 객체는 Persistent만을 위해서 사용한다.

 

 

 

 

 전체 구조(package 기준)

 

 

 Controller(Web)

  • 기능
    • 해당 요청 url에 따라 적절한 view와 mapping 처리
    • @Autowired Service를 통해 service의 method를 이용
    • 적절한 ResponseEntity(DTO)를 body에 담아 Client에 반환
@Controller
@RequestMapping("/")
public class HomeController {
  @GetMapping
  public String home(HttpSession session) {
      if (!SessionUtil.getUser(session).isPresent()) {
          return "login";
      }
      return "index";
  }
}
  • ⬆️ @RestController
    • view가 필요없는 API만 지원하는 서비스에서 사용
    • @RequestMapping 메소드가 기본적으로 @ResponseBody 의미를 가정한다
    • data(json, xml 등) return이 주목적. return ResponseEntity
    • 즉, @RestController = @Controller + @ResponseBody

 

 

 

  Service

  • 기능
    • @Autowired Repository를 통해 repository의 method를 이용
    • 적절한 비즈니스 로직을 처리한다
    • DAO로 DB에 접근하고 DTO로 데이터를 전달받은 다음, 비즈니스 로직을 처리해 적절한 데이터 반환
@Service
public class UserService {
  @Autowired
  private UserRepository userRepository;
  @Resource(name = "bCryptPasswordEncoder")
  private PasswordEncoder bCryptPasswordEncoder;
  @Autowired
  private MessageSourceAccessor msa;

  public User save(UserDto userDto) {
      if (isExistUser(userDto.getEmail())) {
          throw new UserDuplicatedException(msa.getMessage("email.duplicate.message"));
      }
      return userRepository.save(userDto.toEntityWithPasswordEncode(bCryptPasswordEncoder);
  }
}

 

 

 

 

 

🌐 참고 링크

https://gmlwjd9405.github.io/2018/12/25/difference-dao-dto-entity.html

 

 

엔티티 매핑 소개
- 객체와 테이블 매핑 : @Entity , @Table
- 필드와 컬럼 매핑 : @Column
- 기본 키 매핑 : @Id
- 연관관계 매핑 : @ManyToOne , @JoinColumn

 

 

객체와 테이블 매핑

 

@Entity란?

  • @Entity가 붙은 클래스는 JPA가 관리, 엔티티라 한다.
  • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수
  • ⚠️ 주의할 점
    • 기본 생성자 필수 (파라미터가 없는 public 또는 protected 생성자)
    • final 클래스, enum, interface, inner 클래스 사용 X
    • 저장할 필드에 final 사용 X

 

 

@Entity 속성 정리

  • 속성 : name
    • JPA에서 사용할 엔티티 이름을 지정한다
    • 기본 값 : 클래스 이름을 그대로 사용(예: Member)
    • 같은 클래스 이름이 없으면 가급적 기본값을 사용한다.

 


 

 

@Table

  • @Table은 엔티티와 매핑할 테이블 지정
속성 기능 기본값
name 매핑할 테이블 이름 엔티티 이름을 사용
catalog 데이터베이스 catalog 매핑  
schema 데이터베이스 schema 매핑  
uniqueConstraints(DDL) DDL 생성 시에 유니크 제약 조건 생성  

 

 

 


 

 

 

데이터베이스 스키마 자동 생성

  • DDL을 애플리케이션 실행 시점에 자동 생성
  • 테이블 중심 -> 객체 중심
  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
  • 이렇게 생성된 DDL은 개발 장비에서만 사용
  • 생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬 은 후 사용

 

 

데이터베이스 스키마 자동 생성 - 속성

hibernate.hbm2ddl.auto

옵션 설명
create 기존테이블 삭제 후 다시 생성 (DROP + CREATE)
create-drop create와 같으나 종료시점에 테이블 DROP
update 변경분만 반영(운영DB에는 사용하면 안됨)
validate 엔티티와 테이블이 정상 매핑되었는지만 확인
none 사용하지 않음

 

 

 

데이터베이스 스키마 자동 생성 - 주의

  • 🌟 운영 장비에는 절대 create, create-drop, update 사용하면 안된다.
  • 개발 초기 단계는 create 또는 update
  • 테스트 서버는 update 또는 validate
  • 스테이징과 운영 서버는 validate 또는 none

 

 


 

 

필드와 컬럼 매핑

package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

@Entity
public class Member {

 @Id
 private Long id;
 
 @Column(name = "name")
 private String username;
 
 private Integer age;
 
 @Enumerated(EnumType.STRING)
 private RoleType roleType;
 
 @Temporal(TemporalType.TIMESTAMP)
 private Date createdDate;
 
 @Temporal(TemporalType.TIMESTAMP)
 private Date lastModifiedDate;
 
 @Lob
 private String description;
 
 //Getter, Setter…
}

 

 

매핑 어노테이션 정리

hibernate.hbm2ddl.auto

어노테이션 설명
@Column 컬럼 매핑
@Temporal 날짜 타입 매핑
@Enumerated enum 타입 매핑
@Lob BLOB, CLOB 매핑
@Transient 특정 필드를 컬럼에 매핑하지 않음(매핑 무시)

 

 

 

@Column

속성 설명 기본
name 필드와 매핑할 테이블의 컬럼 이름 객체의 필드 이름
insertable, updatable 등록, 변경 가능 여부 TRUE
nullable(DDL) null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다.  
unique(DDL) @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제 약조건을 걸 때 사용한다.  
columnDefinition (DDL) 데이터베이스 컬럼 정보를 직접 줄 수 있다. ex) varchar(100) default ‘EMPTY' 필드의 자바 타입과 방언 정보를 사용해
length(DDL) 문자 길이 제약조건, String 타입에만 사용한다. 255
precision, scale(DDL) BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다). precision은 소수점을 포함한 전체 자 릿수를, scale은 소수의 자릿수 다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정 밀한 소수를 다루어야 할 때만 사용한다. precision=19, scale=2

 

 

 

@Enumerated

자바 enum 타입을 매핑할 때 사용

💡 주의 !! ORDINAL  사용 X

속성 설명 기본값
value - EnumType.ORDINAL: enum 순서를 데이터베이스에 저장

- EnumType.STRING: enum 이름을 데이터베이스에 저장
EnumType.ORDINAL

 

 

 

@Temporal

날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용

참고: LocalDate, LocalDateTime을 사용할 때는 생략 가능(최신 하이버네이트 지원)

속성 설명 기본값
value - TemporalType.DATE: 날짜, 데이터베이스 date 타입과 매핑 (예: 2013–10–11)

- TemporalType.TIME: 시간, 데이터베이스 time 타입과 매핑 (예: 11:11:11)

- TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이 스 timestamp 타입과 매핑(예: 2013–10–11 11:11:11)
 

 

 

 

@Lob

데이터베이스 BLOB, CLOB 타입과 매핑

  • @Lob에는 지정할 수 있는 속성이 없다.
  • 매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
    • CLOB: String, char[], java.sql.CLOB
    • BLOB: byte[], java.sql. BLOB

 

 

@Transient

  • 필드 매핑X
  • 데이터베이스에 저장X, 조회X
  • 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용
@Transient
private Integer temp;

 

 


 

 

기본 키 매핑

기본 키 매핑 어노테이션

  • @Id
  • @GeneratedValue
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

 

 

 

기본 키 매핑 방법

  • 직접 할당 : @Id만 사용
  • 자동 생성 : @GeneratedValue
    • IDENTITY: 데이터베이스에 위임, MYSQL
    • SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용, ORACLE
      • @SequenceGenerator 필요
    • TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
      • @TableGenerator 필요
    • AUTO: 방언에 따라 자동 지정, 기본값

 

 

 

IDENTITY 전략 - 특징

  • 기본 키 생성을 데이터베이스에 위임
  • 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용
    • ex) MySQL의 AUTO_ INCREMENT
  • JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
  • AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행 한 이후에 ID 값을 알 수 있음
  • IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행 하고 DB에서 식별자를 조회

 

IDENTITY 전략 - 매핑

@Entity
public class Member {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;

 

 

 

SEQUENCE 전략 - 특징

  • 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)
  • 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용

 

 

SEQUENCE 전략 - 매핑

@Entity
@SequenceGenerator(
 name = “MEMBER_SEQ_GENERATOR",
 sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
 initialValue = 1, allocationSize = 1)
public class Member {
 @Id
 @GeneratedValue(strategy = GenerationType.SEQUENCE,
 generator = "MEMBER_SEQ_GENERATOR")
 private Long id;

 

 

 

TABLE 전략

  • 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉 내내는 전략
  • 장점: 모든 데이터베이스에 적용 가능
  • 단점: 성능

 

 

TABLE 전략 - 매핑

create table MY_SEQUENCES (
 sequence_name varchar(255) not null,
 next_val bigint,
 primary key ( sequence_name )
)
@Entity
@TableGenerator(
 name = "MEMBER_SEQ_GENERATOR",
 table = "MY_SEQUENCES",
 pkColumnValue = “MEMBER_SEQ", allocationSize = 1)
public class Member {
 @Id
 @GeneratedValue(strategy = GenerationType.TABLE,
 generator = "MEMBER_SEQ_GENERATOR")
 private Long id;

 

 

 

@TableGenerator - 속성

속성 설명 기본값
name 식별자 생성기 이름 필수
table 키생성 테이블명 hibernate_sequences
pkColumnName 시퀀스 컬럼명 sequence_name
valueColumnName 시퀀스 값 컬럼명 next_val
pkColumnValue 키로 사용할 값 이름 엔티티 이름
initialValue 초기 값, 마지막으로 생성된 값이 기준이다. 0
allocationSize 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨) 50
catalog, schema 데이터베이스 catalog, schema 이름  
uniqueConstraint s(DDL) 유니크 제약 조건을 지정할 수 있다.  

 

 

 

권장하는 식별자 전략

  • 기본 키 제약 조건 : null 아님, 유일, 변하면 안된다.
  • 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대 체키)를 사용하자
  • 예를 들어 주민등록번호도 기본 키로 적절하기 않다.
  • 권장: Long형 + 대체키 + 키 생성전략 사용

 

 

👇 아래 강의를 참고하여 작성한 글입니다

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런

저는 야생형이 아니라 학자형인가봐요^^ 활용편 넘어갔다 30% 정도 듣고 도저히 답답해서 기본편을 들어버렸네요^^. 한주 한주 김영한님 강의 들으니 렙업되는 모습을 스스로 느낍니다. 특히 실

www.inflearn.com

 

+ Recent posts

loading