세션과 쿠키는 항상 함께 다니는 이유는, 세션이 동작하는 데 쿠키가 필요한 경우가 많기 때문입니다.


1. 세션과 쿠키의 관계

세션과 쿠키는 역할이 다름

  • 세션은 서버에서 클라이언트를 관리하기 위해 사용하는 데이터입니다.
  • 쿠키는 클라이언트(브라우저)에 데이터를 저장하기 위해 사용하는 기술입니다.

왜 세션에 쿠키가 필요할까?

  • 세션은 클라이언트를 식별하기 위해 세션 ID를 발급합니다.
  • 세션 ID를 클라이언트와 서버 간에 주고받아야 세션 상태를 유지할 수 있습니다.
  • 쿠키는 이 세션 ID를 클라이언트 측에 저장하고, 이후 요청마다 서버로 전달하는 데 사용됩니다.

2. 세션과 쿠키가 항상 같이 다니는 이유

  1. 세션 ID 전달
    • 서버는 클라이언트를 식별하기 위해 세션 ID를 발급하고, 이를 클라이언트에 저장해야 합니다.
    • 세션 ID를 저장하고 서버로 다시 전달하는 가장 일반적인 방법이 쿠키입니다.
    • :
      HTTP/1.1 200 OK
      Set-Cookie: JSESSIONID=abcd1234; Path=/; HttpOnly
      
  2. 자동 관리
    • 쿠키는 브라우저가 자동으로 관리하므로, 클라이언트가 따로 세션 ID를 전송하지 않아도 브라우저가 요청마다 서버로 쿠키를 전송합니다.
    • :
      GET /profile HTTP/1.1
      Host: example.com
      Cookie: JSESSIONID=abcd1234
      
  3. 보안 및 편의성
    • 세션 ID를 URL로 전달하는 방법(예: URL Rewriting)은 보안 문제와 관리상의 불편함이 있습니다.
    • 쿠키를 사용하면 세션 ID가 URL에 노출되지 않아 보안성이 더 높습니다.

3. 세션 사용 시 쿠키를 따로 저장하지 않는 경우

쿠키 없이 세션을 유지하는 방법

  • 쿠키를 사용하지 않고도 세션을 유지할 수 있지만, 이는 덜 일반적이며, 다음과 같은 방식으로 구현됩니다.
  1. URL Rewriting:
    • 세션 ID를 URL에 포함하여 클라이언트와 서버 간에 전달.
    • :
      GET /profile;jsessionid=abcd1234 HTTP/1.1
      
    • 단점:
      • URL이 길어지고 관리가 어려움.
      • 세션 ID가 링크를 통해 외부로 유출될 위험이 있음.
  2. Hidden Field (HTML 폼):
    • 세션 ID를 숨겨진 HTML 입력 필드에 저장하여 폼 데이터와 함께 전송.
    • :
      <form action="/profile" method="POST">
          <input type="hidden" name="jsessionid" value="abcd1234">
          <button type="submit">Submit</button>
      </form>
      
    • 단점:
      • GET 요청에는 사용할 수 없음.
      • 페이지마다 세션 ID를 포함해야 하므로 구현이 복잡.

4. 세션과 쿠키의 공통 허용 여부

웹 브라우저 설정에서 세션과 쿠키가 항상 함께 나오는 이유

  • 쿠키는 세션 ID를 저장하고 전송하는 데 자주 사용되므로, 쿠키가 차단되면 세션도 제대로 동작하지 않을 가능성이 큽니다.
  • 쿠키를 허용하지 않으면 세션 유지가 어려워질 수 있으므로, 브라우저에서 "세션과 쿠키"를 함께 설정 항목으로 묶는 경우가 많습니다.

5. 세션과 쿠키의 차이

  세션(Session) 쿠키(Cookie)
저장 위치 서버 클라이언트(브라우저)
용도 사용자 상태 관리 (예: 로그인) 사용자 데이터 저장 (예: 선호 테마, 언어)
보안성 데이터는 서버에 저장되므로 비교적 안전 클라이언트에 저장되므로 조작/탈취 가능성 있음
데이터 전송 세션 ID만 전송 쿠키 전체 데이터가 요청마다 서버로 전송
유효 기간 서버에서 설정 가능 (일반적으로 세션 유지 동안) 설정된 만료 시간에 따라 유지
상호작용 쿠키를 통해 세션 ID를 전달하는 데 사용 세션 없이도 독립적으로 사용 가능

6. 요약

  • 세션은 서버에서 사용자 상태를 관리하기 위한 데이터이고, 쿠키는 세션 ID를 저장하고 클라이언트와 서버 간에 전송하는 역할을 합니다.
  • 쿠키를 사용하면 세션 ID를 쉽게 관리하고 전송할 수 있으므로, 세션과 쿠키가 자주 함께 사용됩니다.
  • 세션은 반드시 쿠키에 의존하지는 않지만, 쿠키를 사용하지 않으면 구현이 복잡하고 관리가 어려워질 수 있습니다.
    따라서 세션과 쿠키는 **"같이 다니는 것처럼 보인다"**고 느껴질 수 있습니다.

 

'CS ( Computer Science ) > 네트워크 (Networking)' 카테고리의 다른 글

[Net] 네트워크 요약  (1) 2025.01.22
[Net] Filter  (1) 2025.01.01
[Net] Token & JWT  (0) 2024.12.29
[Net] Cookie  (1) 2024.12.28
[Net] MVC 패턴  (0) 2024.12.13

Session

📌 클라이언트와 서버 간의 상태를 유지하기 위해 서버가 클라이언트별로 생성하고 관리하는 데이터입니다.

  • 결국 보안 문제를 해결하려면 중요한 정보는 모두 서버에서 저장해야한다. Client와 서버는 예측이 불가능한 임의의 값으로 연결해야 한다.
  • 서버에서 중요한 정보를 보관하며 로그인 연결을 유지하는 방법을 Session 이라고 한다. 앞서 배운 Cookie는 중요한 정보를 Client측에서 보관하고 있는것이다.

 

Session 생성 순서

 

  1. 로그인에 성공하면 Server에서 임의로 만든 Session ID를 생성한다.
    • Session ID는 예측이 불가능해야 한다.
    • UUID와 같은 값을 활용한다.
  2. 생성된 Session ID와 조회한 User 인스턴스를 서버의 Session 저장소에 저장한다.
    • 서버에 유저와 관련된 중요한 정보를 저장한다.

 

Session 동작 순서

로그인

  • 상태유지를 위해 Cookie를 사용한다.
    • 서버는 클라이언트에 Set-Cookie: SessionId=임의생성값 을 전달한다.
    • 클라이언트는 Cookie 저장소에 전달받은 SessionId 값을 저장한다.
  • Sessions을 사용하면 유저와 관련된 정보는 클라이언트에 없다.

 

로그인 이후 요청

  • 클라이언트는 모든 요청에 Cookie 의 SessionId를 전달한다.
  • 서버에서는 Cookie를 통해 전달된 SessionId로 Session 저장소를 조회한다.
  • 로그인 시 저장하였던 Session 정보를 서버에서 사용한다.

 

Session 특징

  1. Session을 사용하여 서버에서 민감한 정보들을 저장한다.
    • 예측이 불가능한 세션 ID를 사용하여 쿠키값을 변조해도 문제가 없다.
    • 세션 ID에 중요한 정보는 들어있지 않다.
    • 시간이 지나면 세션이 만료되도록 설정한다.
    • 해킹이 의심되는 경우 해당 세션을 제거하면 된다.
  2. Session은 특별한것이 아니라 단지 Cookie를 사용하여 클라이언트가 아닌 서버에서 데이터를 저장해두는 방법이다.
  3. Servlet은 Session 을 자체적으로 지원한다.

 

Servlet의 HttpSession

  • Servlet이 공식적으로 지원하는 Session인 HttpSession은 Session 구현에 필요한 다양한 기능들을 지원한다.

상수를 클래스로 관리하는 방법

// 추상클래스 -> O
public abstract class Const {
	public static final String LOGIN_USER = "loginUser";
}

// 인터페이스 -> O
public interface Const {
	String LOGIN_USER = "loginUser";
}

// 클래스 -> X
public class Const { // new Const();
	public static final String LOGIN_USER = "loginUser";
}

// Const.LOGIN_USER; O
// new Const(); X
  • 상수로 활용할 class는 인스턴스를 생성(new)하지 않는다.
  • abstract 추상클래스 혹은 interface 로 만들어 상수값만 사용하도록 만들어두면 된다.
Servlet을 통해 HttpSession 을 생성하게되면 SessionIdJSESSIONID로 생성되고 JSESSIONID의 Value는 예측 불가능한 랜덤값으로 생성된다.

=
Servlet 환경에서 HttpSession을 생성할 때 서버가 JSESSIONID라는 이름의 쿠키를 발급하고, 그 값이 랜덤하게 생성된 고유 식별자라는 뜻입니다. 이를 통해 세션을 식별하고 사용자와 서버 간의 상태를 유지합니다.
더보기

SessionUserController

@Controller
@RequiredArgsConstructor
public class SessionUserController {

    private final UserService userService;

    @PostMapping("/session-login")
    public String login(
            @Valid @ModelAttribute LoginRequestDto dto,
            HttpServletRequest request
    ) {

        LoginResponseDto responseDto = userService.login(dto.getUserName(), dto.getPassword());
        Long userId = responseDto.getId();

        // 실패시 예외처리
        if (userId == null) {
            return "session-login";
        }

        // 로그인 성공시 로직
        // Session의 Default Value는 true이다.
        // Session이 request에 존재하면 기존의 Session을 반환하고,
        // Session이 request에 없을 경우에 새로 Session을 생성한다.
        HttpSession session = request.getSession();

        // 회원 정보 조회
        UserResponseDto loginUser = userService.findById(userId);

        // Session에 로그인 회원 정보를 저장한다.
        session.setAttribute(Const.LOGIN_USER, loginUser);

        // 로그인 성공시 리다이렉트
        return "redirect:/session-home";
    }

    @PostMapping("/session-logout")
    public String logout(HttpServletRequest request) {
        // 로그인하지 않으면 HttpSession이 null로 반환된다.
        HttpSession session = request.getSession(false);
        // 세션이 존재하면 -> 로그인이 된 경우
        if(session != null) {
            session.invalidate(); // 해당 세션(데이터)을 삭제한다.
        }

        return "redirect:/session-home";
    }
}
  • request.getSession();
  • request.getSession(true);
    • Default 설정
    • Request 객체 내에 Session이 존재한다면 기존 Session을 반환
    • Request 객체 내에 Session이 없으면 새로운 Session을 생성해서 반환
  • request.getSession(false);
    • Request 객체 내에 Session이 존재한다면 기존 Session을 반환
    • Request 객체 내에 Session이 없으면 null을 반환
  • session.setAttribute(Const.LOGIN_USER, responseDto);
    • Session에 Data를 저장하는 방법으로 request.setAttribute(); 와 비슷하다.
    • 하나의 Session에 여러개의 데이터를 메모리에 저장할 수 있다.

 

SessionHomeController

요구사항1. 로그인한 회원이면 home 페이지로 이동한다.
요구사항2. home 페이지를 보려면 로그인이 필수이다.
요구사항3. 로그인하지 않은 회원이면 login 페이지로 이동한다.
@Controller
@RequiredArgsConstructor
public class SessionHomeController {

    private final UserService userService;

    @GetMapping("/session-home")
    public String home(
            HttpServletRequest request,
            Model model
    ) {

        // default인 true로 설정되면 로그인하지 않은 사람들도 값은 비어있지만 세션이 만들어진다.
        // session을 생성할 의도가 없다.
        HttpSession session = request.getSession(false);

        // session이 없으면 로그인 페이지로 이동
        if(session == null) {
            return "session-login";
        }

        // session에 저장된 유저정보 조회
        // 반환타입이 Object여서 Type Casting이 필요하다.
        UserResponseDto loginUser = (UserResponseDto) session.getAttribute(Const.LOGIN_USER);

        // Session에 유저 정보가 없으면 login 페이지 이동
        if (loginUser == null) {
            return "session-login";
        }

        // Session이 정상적으로 조회되면 로그인된것으로 간주
        model.addAttribute("loginUser", loginUser);
        // home 화면으로 이동
        return "session-home";

    }
}

Session은 Memory를 사용하기 때문에 리소스를 낭비하면 안된다.

 

session-home.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Home</title>
</head>
<body>
<h1>Welcome Session Home!</h1>

<p th:text="'안녕하세요, ' + ${loginUser.name} + '님!'">Hello, User!</p>

<!-- 로그아웃 버튼 -->
<form th:action="@{/session-logout}" method="post" style="margin-top: 10px;">
  <button type="submit">Logout</button>
</form>

</body>
</html>

 

session-login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Session Login</title>
</head>
<body>
<h2>Session Login</h2>
<form th:action="@{/session-login}" method="post">
  <div>
    <label for="userName">Username:</label>
    <input type="text" id="userName" name="userName" required>
  </div>
  <div>
    <label for="password">Password:</label>
    <input type="password" id="password" name="password" required>
  </div>
  <button type="submit">Login</button>
</form>

</body>
</html>

 

 

브라우저 테스트

더보기

session-home 페이지 접근

로그인 실패

최초 로그인

이후 로그인

로그아웃

 

 

 

@SessionAttribute

📌 Spring에서는 Session을 쉽게 다루도록 @SessionAttribute라는 어노테이션이 제공된다.

  • request.getSession(true); 와는 다르게 Session을 새로 생성하는 기능은 없다.
  • 이미 로그인이 완료된 사용자를 찾는 경우 즉, Session이 있는 경우에 사용한다.
  • = 세션을 한번이라도 생성한 세션만 사용가능하다
@Controller
@Requiredargsconstructor
public class SessionHomeController {

		private UserService userService;
	
		@GetMapping("/v2/session-home")
    public String homeV2(
            // Session이 필수값은 아니다. 로그인 했거나 안했거나 판별해야하니 required false
            @SessionAttribute(name = Const.LOGIN_USER, required = false) UserResponseDto loginUser,
            Model model
    ) {

        // session에 loginUser가 없으면 Login 페이지로 이동
        if (loginUser == null) {
            return "session-login";
        }

        // Session이 정상적으로 조회되면 로그인된것으로 간주
        model.addAttribute("loginUser", loginUser);

        // home 화면으로 이동
        return "session-home";
    }
}

1. session-home 접속 및 로그인 이후 JSESSIONID 생성 및 Cookie 저장

2. /v2/session-home 에 접속해도 같은 결과를 확인할 수 있다.

  • 처음 로그인을 시도하는 경우
    • 완전히 처음 로그인을 시도하는 경우 URL 뒤에 JSSESSIONID=값이 함께 전달된다.
    • 해당 값은 굳이 필요가 없다. 내부적으로 Set-Cookie로 SessionID와 값을 넣어주기 때문
    • Cookie를 지원하지 않으면 URL을 통해 Session을 유지하는 방법에 사용된다.
      • 하지만, 사용하려면 모든 요청 URL에 jsessionId값이 전달 되어야한다.
Template Engine을 사용하면 jsessionId를 URL에 자동으로 포함한다.

 

URL이 아닌 Cookie를 통해서만 Session을 유지하고자 한다면?

= 클라이언트와 서버 간 세션 상태를 유지하기 위해 세션 ID를 쿠키에 저장하고 사용한다는 뜻입니다.
즉, URL에 세션 ID를 포함하지 않고 쿠키를 사용하여 세션을 식별하고 관리합니다.

 

application.properties

server.servlet.session.tracking-modes=cookie

application.yml

server: 
	servlet: 
		session: 
			tracking-modes: cookie

 

 

Session 정보

📌 HttpSession은 Session을 간편하게 사용할 수 있도록 다양한 기능을 지원한다.

@Slf4j
@RestController
public class SessionController {

    @GetMapping("/session")
    public String session(HttpServletRequest request) {
        HttpSession session = request.getSession(false);

        if (session == null) {
            return "세션이 없습니다.";
        }

        // session 정보 조회
        log.info("session.getId()={}", session.getId());
        log.info("session.getMaxInactiveInterval()={}", session.getMaxInactiveInterval());
        log.info("session.getCreationTime()={}", session.getCreationTime());
        log.info("session.getLastAccessedTime()={}", session.getLastAccessedTime());
        log.info("session.isNew()={}", session.isNew());

        return "세션 조회 성공!";
    }
    
}
  • Postman
    • 로그인

로그 출력

 

session 정보

  1. session.getId();
    • jsessionId 값을 조회할 수 있다.
  2. session.getMaxInactiveInterval();
    • 세션의 유효시간
    • second 단위 default는 30분(1800초)이다.
  3. session.getCreationTime();
    • 세션 생성시간
    ex) Sat Dec 9 15:40:23 KST 2024
  4. session.getLastAccessedTime();
    • 해당 세션에 마지막으로 접근한 시간
    ex) Sat Dec 9 15:40:23 KST 2024
  5. session.isNew();
    • 새로 생성된 세션인지 여부

 

Session TimeOut

📌 사용자가 일정 시간 동안 서버와 상호작용하지 않을 경우, 서버가 해당 세션을 자동으로 만료시키는 기능입니다. 이는 서버 리소스를 효율적으로 관리하고 보안을 강화하기 위해 사용됩니다.

  • Session은 logout 기능을 사용하여 session.invalidate(); 가 되어야 삭제되지만 대부분의 사용자들은 로그아웃을 굳이 하지않고, 브라우저를 종료한다.

 

Session의 문제점

  • HTTP는 Connectionless 특성을 가지고 있어서 서버가 브라우저 종료 여부를 판별하지 못한다.
  • 서버에서 Session을 언제 삭제해야 하는지 판단하기 힘들다.
  • JSESSIONID의 값을 탈취 당한 경우 해당 값으로 악의적인 요청을 할 수 있다.
  • 세션은 서버 메모리에 생성되고 자원은 한정적이기 때문에 꼭 필요한 경우만 생성해야 한다.

ex) 로그인한 유저가 100만명 이라면..?

 

Session 생명주기

  • 기본적으로 30분을 기준으로 세션을 삭제한다.
  • 실제 로그인 후 30분 이상의 시간동안 사용중인 사용자의 세션 또한 삭제된다.
  • 다시 로그인 해야하는 경우가 발생한다.

 

HttpSession 사용

  • 세션 생성시점 30분이 아닌 서버에 최근 Session을 요청한 시간을 기준으로 30분을 유지한다.
  • HttpSession은 기본적으로 해당 방식으로 세션의 생명주기를 관리한다.
  • Session 정보에서 LastAccessedTime 을 기준으로 30분이 지나면 WAS가 내부적으로 세션을 삭제한다.

 

 

Session의 한계

📌 Session은 서버의 메모리를 사용하여 확장성이 제한되어 있다.

  • 사용자가 많아질수록 서버가 관리해야 하는 세션 데이터가 증가하여, 서버의 메모리와 자원이 빠르게 소진됩니다.
오버헤드(Overhead)란? 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간, 메모리 등을 의미한다.

 

Session의 한계

  • 서버가 DB 혹은 메모리에 저장된 세션 정보를 매번 조회하여 오버헤드가 발생한다.
  • 서버가 상태를 유지해야 하므로 사용자 수가 많아질수록 부담이 커진다.
  • Cookie는 웹 브라우저에만 존재하여 모바일 앱 등의 다양한 클라이언트에서 인증을 처리할 수 없다.
  • Scale Out(수평적 확장)에서 서버간 세션 공유가 어렵다.

 

Token

📌 Web Application이나 API에서 인증(Authentication)과 인가(Authorization) 과정에서 사용되며 사용자 또는 시스템의 신원과 권한을 증명하고 요청의 유효성을 검증하는 데 사용되는 디지털 문자열이다.

  • 인증(Authentication), 권한 부여(Authorization), 또는 데이터 교환을 위해 클라이언트와 서버 간에 사용되는 작고 안전한 데이터 객체입니다.
  • 로그인 인증이나 사용자 권한 관리를 할 때 토큰을 발급하고, 이를 통해 사용자를 식별하거나 인증 상태를 유지할 수 있습니다.
  • 토큰은 전체 데이터를 포함하지 않는다. 박스가 아니라 키라고 생각해야한다. 말 그대로 다른 클라에 요청할 시 사용가능한 인물인지에 대한 정보만 포함된다. 패킷이 아니다.

  • Token 생성 시 사용자의 고유한 정보를 포함한다.
  • 데이터베이스에 접근하지 않고 Token의 유효성만 검증한다.
  • Token의 단점
    1. Cookie/Session 방식보다 Token 자체의 데이터 용량이 많다.
      • 요청이 많아지면 그만큼 트래픽이 증가한다.
    2. Payload(전송되는 데이터)는 암호화되지 않아서 중요한 데이터를 담을 수 없다.
    3. Token을 탈취당하면 대처하기 어려워 만료 시간(30분)을 설정한다.

 

0. Token을 사용하는 이유

  1. Token은 서버가 아닌 클라이언트에 저장되어 서버의 부담을 덜 수 있다.
  2. Cookie는 웹 브라우저에만 존재하여 모바일 앱 등의 다양한 클라이언트에서 인증을 처리할 수 없다.
  3. Token 방식은 Stateless를 기반으로 하여 확장성이 뛰어나다.
  4. 인증된 사용자임을 확인하기 위한 고유한 서명을 포함하여 위조된 요청인지 확인할 수 있다.

 

1. 토큰의 기본 개념

토큰의 역할

  1. 인증(Authentication):
    • 사용자가 로그인하면 서버는 인증 결과를 기반으로 토큰을 발급합니다.
    • 클라이언트는 이후 요청마다 이 토큰을 사용하여 인증된 사용자임을 증명합니다.
  2. 권한 부여(Authorization):
    • 사용자가 어떤 리소스에 접근할 수 있는 권한이 있는지 확인합니다.
    • 예: "관리자" 권한을 가진 사용자만 특정 API에 접근 가능.
  3. 상태 정보 저장(State):
    • 토큰에 사용자 관련 정보를 포함하여 서버가 상태를 유지하지 않아도 인증 상태를 확인할 수 있습니다.

2. 토큰의 종류

a. 세션 토큰

  • 서버가 상태를 관리하며, 클라이언트는 세션 ID를 사용해 서버에 요청.
  • 예: session_id=abcd1234 쿠키로 저장.

b. JSON Web Token (JWT)

  • 클라이언트 측에서 상태를 관리할 수 있는 자체 포함(Self-Contained) 토큰.
  • 서명(Signature)을 통해 데이터의 변조를 방지.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsIm5hbWUiOiJKb2huIERvZSJ9.s5t0sT5kdcfXb3wNkm2mOiVjHXe

c. OAuth 토큰

  • OAuth2.0 표준에서 사용하는 토큰으로, **액세스 토큰(Access Token)**과 **리프레시 토큰(Refresh Token)**으로 구성.

d. CSRF 토큰

  • CSRF(Cross-Site Request Forgery) 공격을 방지하기 위해 요청마다 고유한 토큰을 생성.

3. JSON Web Token (JWT)의 구조

JWT는 세 가지 부분으로 구성된 문자열이며, 점(.)으로 구분됩니다.

Header.Payload.Signature

Header (헤더):

  • 토큰의 메타정보를 포함.
  • 예: 알고리즘과 토큰 타입.
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload (페이로드):

  • 사용자 정보와 클레임(Claim)을 포함.
  • 예: 사용자 ID, 권한 정보.
{
  "userId": 1,
  "role": "admin"
}

 

Signature (서명):

  • 토큰의 무결성을 검증하기 위한 서명.
  • 비밀키(Secret Key)로 생성.

4. 토큰 기반 인증 흐름

a. 로그인 요청

  • 사용자가 서버에 로그인 요청을 보냅니다.
POST /login HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "username": "john_doe",
  "password": "secure_password"
}

b. 토큰 발급

  • 서버는 사용자를 인증한 후, JWT 또는 세션 토큰을 발급합니다.
HTTP/1.1 200 OK
Content-Type: application/json

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

c. 요청마다 토큰 사용

  • 클라이언트는 이후 요청마다 토큰을 Authorization 헤더에 포함하여 전송합니다.
GET /dashboard HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

d. 서버에서 토큰 검증

  • 서버는 토큰의 서명을 검증하고, 페이로드에 포함된 사용자 정보를 기반으로 요청을 처리합니다.

 

5. 토큰의 장단점

장점

  1. 확장성:
    • 상태를 서버에 저장하지 않으므로, 분산 서버 환경에서도 쉽게 사용 가능.
  2. 자체 포함(Self-Contained):
    • JWT는 페이로드에 필요한 정보를 포함하므로, 서버가 별도의 데이터베이스를 조회할 필요가 줄어듭니다.
  3. 유연성:
    • 토큰은 쿠키, HTTP 헤더, 또는 URL 파라미터로 전달할 수 있습니다.

단점

  1. 보안 취약성:
    • 토큰이 탈취되면 만료 전까지 악용될 수 있습니다.
    • 해결: HTTPS 사용, 짧은 유효 기간, 리프레시 토큰.
  2. 데이터 크기:
    • JWT는 페이로드에 데이터를 포함하므로, 크기가 커질 수 있습니다.
  3. 만료 처리:
    • JWT는 발급 후 변경할 수 없으므로, 토큰이 만료되거나 폐기된 경우 클라이언트에 새로운 토큰을 발급해야 합니다.

6. 토큰과 세션의 차이

  세션 기반 인증 토큰 기반 인증 (JWT)
저장 위치 서버 (세션 ID는 클라이언트에 저장) 클라이언트 (서버에 상태 저장 없음)
확장성 서버가 상태를 관리하므로 확장에 제약 상태를 저장하지 않으므로 확장성 우수
보안성 서버에서 세션 만료나 폐기가 가능 탈취 시 만료 전까지 악용 가능
데이터 크기 세션 ID만 저장 페이로드 포함, 크기가 상대적으로 큼
사용 사례 로그인 상태 유지 (웹 애플리케이션) API 인증, 분산 시스템

7. 토큰 사용 시 보안 고려 사항

  1. HTTPS 사용:
    • 토큰이 네트워크에서 탈취되지 않도록 HTTPS를 통해 전송.
  2. 짧은 유효 기간 설정:
    • 액세스 토큰은 유효 기간을 짧게 설정하고, 만료 시 리프레시 토큰으로 재발급.
  3. 서명 검증:
    • 서버는 토큰의 서명을 반드시 검증하여 변조 여부를 확인.
  4. 탈취 방지:
    • HttpOnly, Secure 속성을 사용해 쿠키로 저장하거나, 브라우저 로컬 저장소에 저장.

8. 요약

  • **토큰(Token)**은 클라이언트와 서버 간 인증 및 권한 부여를 위한 작은 데이터 객체입니다.
  • JWT는 가장 널리 사용되는 토큰 방식으로, 페이로드에 사용자 정보를 포함하여 자체적으로 인증을 수행.
  • 토큰은 분산 시스템과 API 기반 애플리케이션에서 유용하지만, 보안을 강화하기 위해 짧은 유효 기간과 HTTPS 사용 등 추가 조치를 해야 합니다.

 

JWT(JSON Web Token)

📌 인증에 필요한 정보들을 암호화시킨 JSON 형태의 Token을 의미한다. JSON 데이터 포맷을 사용하여 정보를 효율적으로 저장하고 암호화로 서버의 보안성을 높였다.

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

JWT 구조

Header

  • 토큰의 타입과 해싱 알고리즘을 정의한다.
  • 예시
{
	"alg": "HS256",
	"typ": "JWT"
}

 

Payload

  • 실제로 인증과 관련된 데이터(Claims)를 담고 있다.
  • Claims의 종류
    • Registered Claims : 미리 정의된 Claims
      • iss(issuer) : 발행자
      • exp(expiration time) : 만료시간
      • sub(subject) : 제목
      • iat(issued At) : 발행 시간
      • jti(JWT ID) : 토큰의 고유 식별자
    • Public Claims : 사용자가 정의할 수 있는 클레임, 공개용 정보 전달 목적
    • Private Claims : 사용자 지정 클레임, 당사자들 간에 정보를 공유하기 위한 목적
  • 예시
{
  "sub": "1234567890",
  "name": "Sparta",
  "exp": 1682563600
}

 

Signature

  • Header와 Payload를 서버의 Secret Key로 서명하여 암호화 한다.
  • 암호화는 Header에서 정의한 알고리즘(alg)을 활용한다.
  • 서명을 통해 서버는 Token이 변조되지 않았음을 확인할 수 있다.
  • 예시
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

 

base64UrlEncode는 값을 URL에서 사용할 수 있도록 +, /를 각각 -, _로 표기한다.
Header와 Payload는 Encoding된 값이기 때문에 복호화 혹은 값을 수정할 수 있지만 Signature는 서버에서 관리하는 값이기 때문에 Secret Key가 유출되지 않는 이상 복호화 할 수 없다.

 

 

JWT 인증

📌 JWT는 Base64로 인코딩되어 쉽게 복호화 할 수 있다. Payload가 그대로 노출되기 때문에 비밀번호나 민감한 정보를 저장하지 않는다.

  1. 클라이언트의 로그인 요청
  2. 로그인에 성공했다면 Header, Payload에 Secret Key를 사용하여 Signature를 만든다.
    • 이후 Base64로 Encoding 한다.
    • 일반적으로 Cookie에 담아 클라이언트에게 JWT를 발급한다.
  3. 발급받은 JWT를 저장 후 서버에 요청할 때 Authorization Header에 JWT를 담아 보낸다.
  4. 서버에서 JWT의 유효성 검사를 통해 통과한다면 인증에 성공하여 요청을 처리해준다.
    • JWT 만료, 위변조 여부를 검사한다.

 

JWT의 유효성 검사

  1. A의 JWT를 B가 탈취
  2. B가 탈취한 JWT를 임의로 수정
  3. B가 수정한 JWT로 Server에 요청
  4. 서버는 Signature를 사용하여 유효성 검사(Signature 불일치)
    • Header, Payload를 서버의 Secret Key값을 이용해 Signature를 다시 만들어 비교한다.
    • 임의로 조작된 데이터를 판별할 수 있다.
JSON Web Token의 목적은 정보 보호가 아닌, 위조 방지에 있다.

 

JWT 장점

  1. Signature로 서버의 보안성이 증가한다.
  2. Token 자체가 필요한 정보(유저 및 검증 정보)들을 모두 가지고 있다.
  3. 서버는 인증 정보와 관련된 별도의 저장소를 사용하지 않는다.
  4. 서버의 수평 확장성(Scale Out)이 높아진다.
  5. Cookie가 없는 다른 환경에서도 인증/인가를 적용할 수 있다.
  6. DB를 조회하지 않아도 된다.
Mobile의 경우 App을 자주 닫거나 백그라운드로 전환하여 Session 방식을 사용하지 않는다.

 

 

JWT 단점

  1. Payload는 암호화 된 것이 아니라 민감한 정보를 다루지 못한다.
  2. Token의 길이가 길어서 트래픽이 증가하면 네트워크에 부하가 증가한다.
  3. 클라이언트 측에서 Token을 관리하기 때문에 탈취당하면 대처하기 어렵다.

 

Access Token, Refresh Token

📌 Token은 클라이언트에서 관리하여 탈취당할 위험성이 높기 때문에 만료 시간 설정이 필요하다. 이 때 발생하는 단점을 극복하기 위해 Access Token과 Refresh Token을 사용한다.

  • Token의 유형
    1. Access Token
      • 사용자 인증 후 서버가 발급하는 유저 정보가 담긴 토큰이다.
      • 유효 기간 동안 API나 리소스에 접근할 때 사용한다.
    2. Refresh Token
      • Access Token은 보안을 위해 짧은 수명을 가진다.
      • Access Token이 만료된 경우 재발급 받기위해 사용한다.
      • 주로 데이터베이스에 유저 정보와 같이 저장한다.
  1. 클라이언트의 로그인 요청
  2. 로그인에 성공했다면 Header, Payload에 Secret Key를 사용하여 Signature를 만든다.
  3. 발급받은 JWT를 저장 후 서버에 요청할 때 Authorization Header에 JWT(Access Token)를 담아 보낸다.
  4. 서버에서 JWT의 유효성 검사를 통해 통과한다면 인증에 성공하여 요청을 처리해준다.
  5. Access Token이 만료 되었다면 Refresh Token 으로 토큰 재발급을 요청한다.
  6. 서버로부터 Access Token을 재발급 받는다.
JWT를 Access Token만을 사용하여 인증한다면 탈취되어 보안에 취약할 수 있다. 유효 시간을 부여하여 문제를 해결하지만 유효 시간이 짧다면 로그인을 자주 해야하기 때문에 Refresh Token을 적용한다.
 
참고하면 좋은 영상

 

'CS ( Computer Science ) > 네트워크 (Networking)' 카테고리의 다른 글

[Net] Filter  (1) 2025.01.01
[Net] Session & Cookie의 관계  (0) 2024.12.31
[Net] Cookie  (1) 2024.12.28
[Net] MVC 패턴  (0) 2024.12.13
[Net] API 설계  (0) 2024.12.12

Cookie

📌 사용자의 웹 브라우저에 저장되는 정보로 사용자의 상태 혹은 세션을 유지하거나 사용자 경험을 개선하기 위해 사용된다. 사용자 정보나 세션 데이터를 클라이언트(브라우저)에 저장하는 기술

  • Cookie는 주로 사용자 세션 관리(로그인, 장바구니, 접속시간)나 광고 트래킹(사용자 행동) 등의 목적으로 사용된다.

  1. HTTP는 Stateless, Connectionless 특성을 가지고 있다.
  2. Client가 재요청시 Server는 이전 요청에 대한 정보를 기억하지 못한다.
  3. 로그인과 같이 상태를 유지해야 하는 경우가 발생한다.
  4. Request에 사용자 정보를 포함하면 해결이 된다.
    • 로그인 후에는 사용자 정보와 관련된 값이 저장되어 있어야한다.
  5. 브라우저를 완전히 종료한 뒤 다시 열어도 사용자 정보가 유지되어야 한다.
서버에 전송하지 않고 브라우저에 단순히 데이터를 저장하고 싶다면 Web Storage(localStorage, sessionStorage)를 사용하면 된다. 하지만 보안에 취약하기 때문에 주민번호와 같은 민감정보를 저장하면 안된다.

Web Storage는 클라이언트의 저장소로, 쿠키와 비교했을 때 서버로 데이터를 자동전송하지 않으며, 클라이언트에서만 데이터를 유지한다는 특징을 가지고 있다.

 

 

  쿠키 (Cookie) Web Storage (localStorage/sessionStorage)
저장 위치 클라이언트(브라우저), HTTP 요청 시 서버로 전송 클라이언트(브라우저), 서버로 자동 전송되지 않음
데이터 전송 HTTP 요청 시 서버로 자동 전송 서버로 전송되지 않음
저장 용량 약 4KB 약 5MB
유효 기간 설정된 만료 시간까지 유지 localStorage는 영구, sessionStorage는 세션 종료 시 삭제
범위 도메인/경로 단위로 설정 가능 localStorage는 모든 탭에서 공유, sessionStorage는 현재 탭에 한정
보안 HttpOnly와 Secure로 보안 강화 가능 자바스크립트를 통해 접근 가능 (XSS 공격에 취약)
주 용도 사용자 상태 유지 (예: 로그인, 인증) 사용자 설정, UI 상태, 임시 데이터 저장
데이터 접근 방식 HTTP 헤더 또는 자바스크립트 API로 접근 자바스크립트 API로 접근
지원 브라우저 모든 브라우저에서 지원 최신 브라우저에서 지원
삭제 방식 쿠키 만료 시간 또는 브라우저 설정에서 삭제 localStorage.clear(), sessionStorage.clear()
  • Cookie 찾아보기
    • 브라우저 개발자도구(F12) → Application → Cookies

 

로그인 성공시 응답

Set-Cookie

  • 로그인시 전달된 ID, Password로 User 테이블 조회하여 일치여부 확인
  • 일치한다면 Set-Cookie를 활용해 Cookie에 사용할 값 저장
    • Cookie는 보안에 취약하다.

 

로그인 이후 요청

요청 헤더 Cookie : 사용자 정보

  • 로그인 이후에는 모든 요청마다 Request Header에 항상 Cookie 값을 담아서 요청한다.
    • 클라이언트 → 서버 방향의 요청
    • 네트워크 트래픽이 추가적으로 발생된다.
    • 최소한의 정보만 사용해야한다.
  • Cookie에 담겨있는 값으로 인증/인가 를 진행한다.

 

Cookie Header

📌 클라이언트(브라우저)가 서버에 HTTP 요청을 보낼 때, 클라이언트에 저장된 쿠키를 함께 전송하는 HTTP 요청 헤더입니다.

  • 서버에서는 HTTP 응답 헤더에 Set-Cookie 속성을 사용해 생성하고 설정할 수 있다
  • Cookie는 서버에서 생성되어 클라이언트에 전달, 저장된다.

 

Cookie Header

  1. Set-Cookie
    • Server에서 Client로 Cookie 전달(Response Header)
  2. Cookie
    • Client가 Cookie를 저장하고 HTTP 요청시 Server로 전달(Request Header)

Response 알아보기

set-cookie: 
sessionId=abcd; 
expires=Sat, 11-Dec-2024 00:00:00 GMT;
path=/; 
domain=spartacodingclub.kr;
Secure
  • Cookie의 생명주기
    1. 세션 Cookie
      • 만료 날짜를 생략하면 브라우저 완전 종료시 까지만 유지된다.(Default)
        • expires, max-age 가 생략된 경우
      • 브라우저를 완전 종료 후 다시 페이지를 방문했을 때 다시 로그인을 해야한다.
    2. 영속 Cookie
      • 만료 날짜를 입력하면 해당 날짜까지 유지한다.
        • expires=Sat, 11-Dec-2024 00:00:00 GMT;
          • 해당 만료일이 도래하면 쿠키가 삭제된다.
        • max-age=3600 (second, 3600초는 한시간. 60 * 60)
          • 0이 되거나 음수를 지정하면 쿠키가 삭제된다.
  • Cookie의 도메인
    • 쿠키가 아무 사이트에서나 생기고 동작하면 안된다!
      • 필요없는 값 전송, 트래픽 문제 등이 발생한다.
    • domain=spartacodingclub.kr
      • domain=spartacodingclub.kr를 지정하여 쿠키를 저장한다.
      • dev.spartacodingclub.kr와 같은 서브 도메인에서도 쿠키에 접근한다.
    • domain을 생략하면 현재 문서 기준 도메인만 적용한다.
  • Cookie의 경로
    • 1차적으로 도메인으로 필터링 후 Path가 적용된다.
    • 일반적으로 path=/ 루트(전체)로 지정한다.
    • 위 경로를 포함한 하위 경로 페이지만 쿠키에 접근한다.
      • path=/api 지정
        • path=/api/example 가능
        • path=/example 불가능
  • Cookie 보안
    1. Secure
      • 기본적으로 Cookie는 http, https 구분하지 않고 전송한다.
      • Secure를 적용하면 https인 경우에만 전송한다. s = Secure
    2. HttpOnly
      • XSS(Cross-site Scripting) 공격을 방지한다.
        • 악성 스크립트를 웹 페이지에 삽입하여 다른 사용자의 브라우저에서 실행되도록 하는 공격
      • 자바스크립트에서 Cookie 접근을 못하도록 막는다.
      • HTTP 요청시 사용한다.
    3. SameSite
      • 비교적 최신 기능이라 브라우저 지원여부를 확인 해야한다.
      • CSRF(Cross-Site Request Forgery) 공격을 방지한다.
        • 사용자가 의도하지 않은 상태에서 특정 요청을 서버에 전송하게 하여 사용자 계정에서 원치 않는 행동을 하게 만든다.
      • 요청 도메인과 쿠키에 설정된 도메인이 같은 경우만 쿠키 전송

 

 

Cookie로 로그인 상태 유지하기

 

  • 한번 로그인에 성공하면 HTTP Response에 쿠키를 담아서 브라우저에 전달한다.
  • 브라우저는 요청마다 Cookie를 함께 전송한다.
  • 보안상의 문제로 name=원욱이 아닌 userId=1과 같은 index 정보를 저장한다.
    • 이것 또한 보안문제가 있다.
  • 요구사항에 맞추어 세션 Cookie를 사용할지 영속 Cookie를 사용할지 결정한다.

  • 코드예시
    • 예시를 위해 ViewTemplate(Thymeleaf)을 사용하는 경우를 가정(SSR)
더보기

User 클래스

@Getter
public class User {
    // 식별자
    private Long id;
    // 이름
    private String name;
    // 나이
    private Integer age;
    // 로그인 ID
    private String userName;
    // 비밀번호
    private String password;

    public User(Long id, String name, Integer age, String userName, String password) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.userName = userName;
        this.password = password;
    }
}
  • User 클래스 설계

로그인 요청 DTO

// 필드 전체를 매개변수로 가진 생성자가 있어야 @ModelAttribute가 동작한다.
@Getter
@AllArgsConstructor
public class LoginRequestDto {
	// 사용자가 입력한 아이디
	@NotBlank	
	private final String userName;
	// 사용자가 입력한 비밀번호
	@NotNull
	private final String password;
}
  • 일반적으로 DTO는 클라이언트의 요청혹은 서버의 응답이기 때문에 변경되면 안된다.
    • final을 사용하여 불변 객체로 관리한다.
    • Java17 버전에 나온 record를 사용할 수 있다.

로그인 응답 DTO

@Getter
public class LoginResponseDto {
    private final Long id;
    // 이외 응답에 필요한 데이터들을 필드로 구성하면 된다.
    // 필요한 생성자
    public LoginResponseDto(Long id) {
        this.id = id;
    }
}

 

유저 조회 응답 DTO

@Getter
public class UserResponseDto {
		// 유저 식별자
    private final Long id;
    // 유저 이름
    private final String name;

    public UserResponseDto(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

 

 

HomeController

@Controller
@RequiredArgsConstructor
public class HomeController {

    private final UserService userService;

    @GetMapping("/home")
    public String home(
            // @CookieValue(required = true) 로 필수값(default) 설정
            // required = false 이면 필수값 아님.
            @CookieValue(name = "userId", required = false) Long userId, // String->Long 자동 타입컨버팅
            Model model
    ) {

        // 쿠키에 값이 없으면 로그인 페이지로 이동 -> 로그인 X
        if(userId == null) {
            return "login";
        }

        // 실제 DB에 데이터 조회 후 결과가 없으면 로그인 페이지로 이동 -> 일치하는 회원정보 X
        UserResponseDto loginUser = userService.findById(userId);

        if(loginUser == null) {
            return "login";
        }

        // 정상적으로 로그인 된 사람이라면 View에서 사용할 데이터를 model 객체에 데이터 임시 저장
        model.addAttribute("loginUser", loginUser);
        // home 화면으로 이동
        return "home";
    }
}

View에서는 model 객체에 담겨있는 loginUser 를 활용하여 변수로 사용할 수 있다.

 

UserController

@Controller
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/login")
    public String login(
            @Valid @ModelAttribute LoginRequestDto request,
            HttpServletResponse response // 쿠키값 세팅에 필요
    ) {
        // 로그인 유저 조회
        LoginResponseDto responseDto = userService.login(request.getUserName(), request.getPassword());

        if (responseDto.getId() == null) {
            // 로그인 실패 예외처리
            return "login";
        }

        // 로그인 성공 처리
        // 쿠키 생성, Value는 문자열로 변환하여야 한다.
        Cookie cookie = new Cookie("userId", String.valueOf(responseDto.getId()));

        // 쿠키에 값 세팅 (expire 시간을 주지 않으면 세션쿠키가 됨, 브라우저 종료시 로그아웃)
        // Response Set-Cookie: userId=1 형태로 전달된다.
        response.addCookie(cookie);
        
        // home 페이지로 리다이렉트
        return "redirect:/home";
    }

    @PostMapping("/logout")
    public String logout(
            HttpServletResponse response
    ) {
        Cookie cookie = new Cookie("userId", null);
        // 0초로 쿠키를 세팅하여 사라지게 만듬
        cookie.setMaxAge(0);
        response.addCookie(cookie);

        // home 페이지로 리다이렉트
        return "redirect:/home";
    }

}
  1. 로그인 기능
    • 로그인에 성공하면 Cookie를 생성하고 HttpServletResponse 객체에 담는다.
      • Cookie 이름(Key)은 userId , 값(Value)은 회원 index 값을 담아둔다.
      • Set-Cookie: userId=1
    • 만료 시간을 지정하지 않으면 세션 쿠키로 만들어진다.
      • 브라우저 종료 전까지 userId 가 모든 요청 헤더의 Cookie에 담겨서 전달된다.
  2. 로그아웃 기능
    • 새로운 Cookie를 userId = null로 생성한다.
    • setMaxAge(0) 설정으로 만료시킨다.
    • 응답에 만료된 쿠키를 담아 보낸다.

UserService

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    public LoginResponseDto login(String userName, String password) {
        // 입력받은 userName, password와 일치하는 Database 조회
        Long index = userRepository.findIdByUserNameAndPassword(userName, password);

        return new LoginResponseDto(index);
    }

    public UserResponseDto findById(Long id) {

        return userRepository.findById(id);
    }
}

 

UserRepository

@Repository
public class UserRepository {

    private static final User USER1 = new User(1L, "wonuk", 100, "wonuk", "1234");
    private static final User USER2 = new User(2L, "wonuk2", 200, "wonuk2", "2345");
    private static final List<User> USERS = Arrays.asList(USER1, USER2);

    public Long findIdByUserNameAndPassword(String userName, String password) {
        return USERS.stream()
                .filter(user -> user.getUserName().equals(userName) && user.getPassword().equals(password))
                .map(User::getId)
                .findFirst()
                .orElse(null);
    }

    public UserResponseDto findById(Long id) {
        return USERS.stream()
                .filter(user -> Objects.equals(user.getId(), id))
                .map(user -> new UserResponseDto(user.getId(), user.getName()))
                .findFirst()
                .orElse(null);
    }
}

실제 Database와 연동하지 않고, 상수로 미리 만든 User를 사용한다.

 

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<h2>Login</h2>
<form th:action="@{/login}" method="post">
    <div>
        <label for="userName">Username:</label>
        <input type="text" id="userName" name="userName" required>
    </div>
    <div>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required>
    </div>
    <button type="submit">Login</button>
</form>

</body>
</html>

 

home.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Home</title>
</head>
<body>
<h1>Welcome Home!</h1>

<p th:text="'안녕하세요, ' + ${loginUser.name} + '님!'">Hello, User!</p>

<!-- 로그아웃 버튼 -->
<form th:action="@{/logout}" method="post" style="margin-top: 10px;">
    <button type="submit">Logout</button>
</form>

</body>
</html>

 

 

브라우저 테스트

http://localhost:8080/home

  • HomeController 호출
  • Cookie에 값이 없는 상태로 login 페이지가 반환된다.

DB에 저장된 userName과 password로 로그인

 

  • 로그인이 실패하면 login 페이지로 이동한다.
  • 로그인이 성공하면 home 페이지로 리다이렉트
  • 리 다이렉트되어 GET + /home 호출
  • Cookie에 저장된 user 식별자 값으로 DB 조회
  • 조회된 User를 Model에 추가
  • home 페이지에서 Model을 참조하여 화면 출력

 

 

Cookie 문제점

📌 Cookie는 보안에 취약하여 userId=1 형태의 방식으로 로그인을 구현하지 않는다.

  • = 쿠키에 민감한 정보를 직접 저장하거나 이를 기반으로 인증 로직을 구현하지 않는다
  • userId=1은 데이터를 숨기지 않고 직접 대놓고 저장하는 방식

 

Cookie 문제점

쿠키 값은 임의로 변경할 수 있다.

  • Client가 임의로 쿠키의 값을 변경하면 서버는 다른 유저로 인식한다.
  • userId = 임의로 수정

  • 브라우저 개발자도구(F12) → Application → Cookies → 값 수정 가능
  • 실제로는 암호화되어 저장되어있는 Value들을 볼 수 있다!

Cookie에 저장된 Data는 탈취되기 쉽다.

  • userId = 주민번호, userId = 인덱스 값
  • 쿠키는 네트워크 전송 구간에서 탈취될 확률이 매우 높다.
    • HTTPS를 사용하는 이유 중 하나에 속한다.
    • 민감한 정보를 저장하면 안된다.
  • 한번 탈취된 정보는 변경이 없다면 반영구적으로 사용할 수 있다.

 

보안 대처방법

  1. 쿠키에 중요한값을 저장하지 않는다.
  2. 사용자 별로 일반 유저나 해커들이 알아보지 못하는 값을 노출한다.
    • 일반적으로 암호화된 Token을 쿠키에 저장한다.
    • 서버에서 암호화된 Token과 사용자를 매핑해서 인식한다.
    • Token은 서버에서 관리한다.
  3. 토큰은 해커가 임의의 값을 넣어도 동작하지 않도록 만들어야 한다.
  4. 해커가 토큰을 탈취해도 사용할 수 없도록 토큰 만료시간을 짧게 설정한다.
  5. 탈취가 의심되는 경우 해당 토큰을 강제로 만료시키면 된다.
    • 접속기기 혹은 IP가 다른 경우 등

 

 

 

'CS ( Computer Science ) > 네트워크 (Networking)' 카테고리의 다른 글

[Net] Session & Cookie의 관계  (0) 2024.12.31
[Net] Token & JWT  (0) 2024.12.29
[Net] MVC 패턴  (0) 2024.12.13
[Net] API 설계  (0) 2024.12.12
[Net] Rendering  (1) 2024.12.05
 

GitHub - sparta-sixsense/newsfeed

Contribute to sparta-sixsense/newsfeed development by creating an account on GitHub.

github.com

 

 

 

 

KEEP

📌 현재 만족하고 계속 이어가고 싶은 = 유지할 부분

  • 나를 위한 커밋이 아니라 팀원을 위한 커밋을 하기 위해 메시지 내용을 어떻게 쓸 지 고민해
  • 프로젝트를 진행하면서 활용하면 좋을 것 같은 다양한 로직과 기술을 사용하고 공유함 (코드 컨벤션, 소스패키지 구조 시각화 등)
  • 정규표현식과 regxp 를 사용해 입력패턴을 적용해 봄
  • 팀의 분위기를 긍정적으로 이끌어가기 위해 노력함
  • 깃 컨벤션을 미리 정해놓은 부분이 좋았음
  • 코드의 안정성을 위해 공통 기능을 우선적으로 준비하고 프로젝트를 시작했던 점이 추후 도움이 되었음
  • 비즈니스 로직 간소화
  • 검증 로직은 별도 메서드로 이원화
  • 팀원들과 화목한 관계 추구 노력

 

 

PROBLEM

📌 불편하게 느꼈고 수정하고 싶은 부분 = 문제였던 부분

  • 선행학습이 부족해 다른 팀원이 사용하는 기술을 이해하지 못함
  • 계획을 세우지 않아 시간을 효율적으로 관리하지 못하고, 개인 학습 시간을 전혀 가지지 못함
  • 깃허브 커멋 컨벤션이 엄격하게 지켜지지 않음
  • 테이블 설계시, 처음에 고려치 못한 부분에 대해 우선순위에서 밀렸다는 이유로 반영 못함
  • 다른 브랜치에 푸쉬하는 실수를 저질렀음
  • 실제 애플리케이션 배포까지 경험해보자라는 목표로 프로젝트에 임했지만 AWS에 대한 이해 부족으로 배포 단계까지 가지 못함
  • 예외 처리를 다소 복잡함. 어느 수준까지 분기할 건지 고민 필요
  • API Url 설계할 때 조금 더 RESTful하게 만들 필요가 있음. url path만 보고 어떤 역할을 하는지 이해하기 쉽지 않은 API 다수
  • 소규모 프로젝트라, 패키지 구조를 [controller, service, repository, domain, dto ...] 이런 식으로 만들었는데, 생각보다 DTO 클래스가 많아 조금만 더 프로젝트 규모가 커지면 유지보수 불가. domain 단위 패키지 구조로 마이그레이션 고려

 

 

TRY

📌 문제 해결을 위해 실행 가능한 것들 = 앞으로의 목표

  • 혼자 해보기엔 어려웠던 기능을 팀원들과 협업하면서 시도해보기
  •  
  • 계획표를 꼭 세우고, 개인 학습 시간 챙기기
  • 다음에는 컨벤션을 더 확실히 정하고 프로젝트를 시작하기
  • 타 팀의 잘한 점을 최대한 흡수하기, 새로운 인사이트를 얻기
  • 우선은 필요 기능을 모두 구현하고, 디테일을 살리기
  • 데이터베이스 설계나 sql에 대해 공부를 더 해야겠다는 생각이 들었음
  • 기능을 왜 쓰는지, 어떻게 쓰는건지 정확히 파악하는 것이 가장 중요하다는 것을 알게됨
  • 다음 프로젝트에서는 간트 차트를 작성해 보고자 함
  • 테스트 코드를 작성
  • 더미데이터를 정합성 있게 제조
  • 테스트 코드가 github에서 자동으로 빌드되고, 애플리케이션이 자동으로 서버에 배포되는 단계까지 적용
  • AWS, Docker, 쿠버네티스, 엘라스틱 서치, 레디스, 카프카 적용
  • 깃에 대한 상세 학습

 

[ 참고 자료 ]

 

팀 문화의 탄생 | 우아한형제들 기술블로그

안녕하세요, 우아한형제들 상품시스템팀의 손권남입니다. 가끔씩 저는 우리팀의 팀 문화에 대한 질문을 받곤 합니다. 그때마다 매번 단편적인 답을 드리곤 하면서 한 번 정도 우리의 팀문화를

techblog.woowahan.com

 

 

'Project > Spring' 카테고리의 다른 글

Spring Querydsl 과제 회고  (1) 2025.01.27
[Spring] IOC & DI  (0) 2025.01.23
일정표를 만들어 보자! 업데이트!  (2) 2024.12.19
일정표를 만들어 보자!  (2) 2024.12.09
[ Spring ] 쇼핑몰 프로젝트 회고  (0) 2024.11.25

Validation

📌 특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 단계를 의미한다.

  • Controller의 주요한 역할 중 하나는 Validation 이다. HTTP 요청이 정상인지 검증한다.

 

 

Validation을 사용하는 이유

  • 주문서 작성 페이지에서 잘못된 입력값으로 인해 서버에 오류가 발생한다면?
  • ex) 휴대폰 번호에 숫자가 아닌 문자가 들어간 경우
  • 서버의 문제로 인해 작성 페이지에서 Error 페이지로 이동된다면?
  • Error 페이지로 이동되어 작성중인 폼이 모두 리셋되어 다시 작성해야 한다면?
  • 이러한 서비스의 유저는 굉장한 불편함을 겪게된다.

 

Validation의 역할

  1. 검증을 통해 적절한 메세지를 유저에게 보여주어야 한다.
  2. 검증 오류로 인해 정상적인 동작을 하지 못하는 경우는 없어야 한다.
  3. 사용자가 입력한 데이터는 유지된 상태여야 한다.

 

실제 서버를 운영하다보면 기능의 의도와 다른 다양한 사용 방법들을 보게된다. ex) Enter로 입력이 완료되도록 만들었지만 누군가는 Click, Tab + Enter를 누르듯

 

검증의 종류

  1. 프론트 검증
    • 해당 검증은 유저가 조작할 수 있음으로 보안에 취약하다.
    • 보안에 취약하지만 그럼에도 꼭 필요하다
    • ex) 비밀번호에 특수문자가 포함되어야 한다면 즉각적인 alert 가능 → 유저 사용성 증가
  2. 서버 검증
    • 프론트 검증없이 서버에서만 검증한다면 유저 사용성이 떨어진다.
    • API Spec을 정의해서 Validation 오류를 Response 예시에 남겨주어야 한다.
      • API 명세서를 잘 만들어야 그에 맞는 대응을 할 수 있다.
    • 서버 검증은 선택이 아닌 필수이다.
  3. 데이터베이스 검증
    • Not Null, Default와 같은 제약조건을 설정한다.
    • 최종 방어선의 역할을 수행한다.
기본적으로 프론트, 서버, 데이터베이스 모두 검증을 꼼꼼하게 하는것이 바람직하다. Validation으로 수많은 Error와 문제들을 방지할 수 있다.

 

 

BindingResult

📌 Spring에서 기본적으로 제공되는 Validation 오류를 보관하는 객체이다. 주로 사용자 입력 폼을 검증할 때 많이 쓰이고 Field Error와 ObjectError를 보관한다.

  • Errors 인터페이스를 상속받은 인터페이스이다.
  • Errors 인터페이스는 에러의 저장과 조회 기능을 제공한다.
  • BindingResult는 addError() 와 같은 추가적인 기능을 제공한다.
  • Spring이 기본적으로 사용하는 구현체( 특정 인터페이스나 추상 클래스를 실질적으로 구현한 클래스 )는 BeanPropertyBindingResult 이다.

 

파라미터에 BindingResult가 없는 경우

@Data
public class MemberCreateRequestDto {
    private Long point;
    private String name;
    private Integer age;
}

// View 반환
@Controller
public class BingdingResultController {
	
    @PostMapping("/v1/member")
    public String createMemberV1(@ModelAttribute MemberCreateRequestDto request, Model model) {
        // Model에 저장
        System.out.println("/V1/member API가 호출되었습니다.");
        model.addAttribute("point", request.getPoint());
        model.addAttribute("name", request.getName());
        model.addAttribute("age", request.getAge());

        // Thymeleaf Template Engine View Name
        return "complete";
    }

}

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Member 생성이 완료되었습니다!</h1>
    <ul>
        <li><span th:text="${point}">포인트</span></li>
        <li><span th:text="${name}">이름</span></li>
        <li><span th:text="${age}">나이</span></li>
    </ul>
</body>
</html>

 

파라미터에 BindingResult가 있는 경우

@Controller
public class BindingResultController {

    @PostMapping("/v2/member")
    public String createMemberV2(
            // 1. @ModelAttribute 뒤에 2. BindingResult가 위치한다.
            @ModelAttribute MemberCreateRequestDto request,
            BindingResult bindingResult,
            Model model
    ) {

        System.out.println("/V2/member API가 호출되었습니다.");

        // BindingResult의 에러 출력
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        System.out.println("allErrors = " + allErrors);

        // Model에 저장
        model.addAttribute("point", request.getPoint());
        model.addAttribute("name", request.getName());
        model.addAttribute("age", request.getAge());

        return "complete";
    }
    
}

@ModelAttribute는 파라미터를 필드 하나하나에 바인딩한다. 파라미터에 Binding Result가 함께 있는 경우 만약 그중 하나의 필드에 오류가 발생하면 해당 필드를 제외하고 나머지 필드들만 바인딩 된 후 Controller가 호출된다.

 

 

Bean Validation

📌 특정 필드 검증의 경우 빈값, 길이, 크기, 형식 과 같은 간단한 로직이다. 이러한 로직들을 모든 프로젝트에 적용할 수 있도록 표준화 한 것이 Bean Validation이다.

@PostMapping("/v3/member")
public String createMemberV3(
        // 1. @ModelAttribute 뒤에 2. BindingResult가 위치한다.
        @ModelAttribute MemberCreateRequestDto request,
        BindingResult bindingResult,
        Model model
) {

    System.out.println("/V3/member API가 호출되었습니다.");

    // 3. Validation
    if (request.getAge() == null || request.getAge() < 0) {
        // BindingResult FieldError 추가
        bindingResult.addError(
                new FieldError("request", "age", "age 필드는 필수이며 0 이상의 값이어야 합니다.")
        );
    }

    // error 처리
    if (bindingResult.hasErrors()) {
        System.out.println("Error를 처리하는 로직");
        // error 페이지 반환
        return "error";
    }

    // Model에 저장
    model.addAttribute("point", request.getPoint());
    model.addAttribute("name", request.getName());
    model.addAttribute("age", request.getAge());

    return "complete";
}

실행결과

  • 검증 기능을 매번 BingdingResult 예시처럼 코드로 구현하는것은 번거로운 일이다.
  • 이 경우 Controller의 크기가 너무 커지며 단일 책임 원칙에 위배된다.
  • 개발자들은 불편함을 참지 않는다.

 

Bean Validation

  • 객체의 필드나 메서드에 제약 조건을 설정하여, 올바른 값을 가지고 있는지 검증하는 표준화된 방법
  1. Bean Validation은 기술 표준 인터페이스이다.
  2. 다양한 Annotation들과 여러가지 Interface로 구성되어 있다.
    • Bean Validation(인터페이스) 구현체인 Hibernate Validator를 사용한다.

Hibernate Validator 공식문서

 

The Bean Validation reference implementation. - Hibernate Validator

Express validation rules in a standardized way using annotation-based constraints and benefit from transparent integration with a wide variety of frameworks.

hibernate.org

Hibernate Validator의 Hibernate는 ORM JPA Hibernate와 이름만 같고 상관없는 기술이다.

 

 

Bean Validation 적용 예시

@Getter
public class SignUpRequestDto {
	
	@NotBlank
	private String name;

	@NotNull
	@Range(min = 1, max = 120)
	private Integer age;

}

@Controller
public class BeanValidationController {

		@PostMapping("/model-attribute")
    public String beanValidationV1(
            @Validated @ModelAttribute SignUpRequestDto dto
    ) {
        // 로직
        
        // ViewName
        return "complete";
    }

}

@RestController
public class BeanValidationRestController {

		@PostMapping("/request-body")
    public String beanValidationV2(
            @Validated @RequestBody SignUpRequestDto dto
    ) {
        // 로직
        
				// 문자 Data 반환
        return "회원가입 완료";
    }

}
  • Annotation을 적용시키는것 만으로 Validation을 아주 쉽게 적용할 수 있다.
  • Controller에 개발자가 기본적인 검증 로직을 작성할 필요가 없어졌다.
  • RestController의 @RequestBody에도 사용할 수 있다.

 

 

Field Error

📌 스프링의 유효성 검증(Validation) 과정에서 발생하는 필드 수준의 에러 정보를 나타내는 객체

  • 유효성 검증 실패 시, 스프링은 각 필드에 대한 검증 결과를 FieldError 객체로 저장하며, 이를 통해 어떤 필드에서 어떤 에러가 발생했는지를 확인 가능

 

Bean Validation 적용

  • 코드예시
    • 의존성 추가(build.gradle)
implementation 'org.springframework.boot:spring-boot-starter-validation'
  • implementation 뒤에 version이 따로 적혀있지 않은 이유 → Spring Boot 기능
  • External Libraries
    • 프로젝트에 적용된 라이브러리 목록

  • 파일경로 jakarta.validation-api-${version}.jar
    • jakarta.validation.constraints

  • 사용할 수 있는 다양한 Annotation들을 확인할 수 있다.
  • 파일경로 org.hibernate.validator:hibernate-validator:${version}
    • 위 Annotation 들이 동작하게 만들어주는 Validator

  1. jakarta.validation-api : Interface
  2. org.hibernate.validator:hibernate-validator : 구현체

 

실제코드 import

  • @Range Annotation은 Hibernate Validator 에서만 동작하는것
  • @NotBlank, @NotNull 은 validation 표준 인터페이스

 

사용된 Annotation 정리

  • @NotBlank
    1. null을 허용하지 않는다.
    2. 공백(” “)을 허용하지 않는다. 하나 이상의 문자를 포함해야한다.
    3. 빈값(””)을 허용하지 않는다.
    4. CharSequence 타입 허용
      • String은 CharSequence(Interface)의 구현체이다.
  • @NotNull
    1. null을 허용하지 않는다.
    2. 모든 타입을 허용한다.
  • @NotEmpty
    1. null을 허용하지 않는다.
    2. 빈값(””)을 허용하지 않는다.
    3. CharSequence, Collection, Map, Array 허용
@Data
public class TestDto {
	// 테스트 하고싶은 Annotation으로 변경 가능
	@NotBlank
	private String stringField;
	
	@NotNull
	@Range(min = 1, max = 9999)
	private Integer integerField;
	

}

import static org.assertj.core.api.Assertions.assertThat;

public class BeanValidationTest {
	
	@Test
	void beanValidation() {
		
		// Spring과 통합하면 아래 두줄의 코드는 사용하지 않는다.
		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
		Validator validator = factory.getValidator();
		
		// Test 하고싶은 상황을 만들어서 검증 가능
		TestDto dto = new TestDto();
		dto.setStringField(" ");
		dto.setIntegerField(1);
		
		// DTO를 검증
		Set<ConstraintViolation<TestDto>> violations = validator.validate(dto);

		// 검증 결과가 예상대로 발생했는지 확인
		// 검증에 걸린 필드가 있어야 함
    assertThat(violations).isNotEmpty();
    // 2개의 제약 위반 발생
    assertThat(violations.size()).isEqualTo(2);
		
		// Validation에 걸린 내역을 출력
		for(ConstraintViolation<TestDto> violation : violations) {
			// 아래의 결과에 Message가 있으면 Validation에 걸린것.
			// Default Message가 있기 때문에 출력됨
			// Message를 수정하고싶다면 Annotation 속성값(message="입력")으로 설정할 수 있다.
			// 결과가 비어있으면 Validation에 걸리지 않은것.
			System.out.println("violation = " + violation.getMessage());
		}
		
	}

}

 

출력결과

  • 테스트 통과
  • Default Message가 출력된다.
 

Hibernate Validator 8.0.1.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone. To avoid duplication of th

docs.jboss.org

import가 org.hibernate.validator로 시작하면 하이버네이트를 사용할 때만 제공되는 검증 기능으로 다른 구현체로 validator를 교체하였을 경우 동작하지 않는다. 하지만 org.hibernate.validator를 대부분 사용한다.

 

 

Validator

📌 유효성 검증(Validation) 인터페이스입니다. 특정 객체에 대한 데이터 유효성을 검사하고, 유효하지 않은 경우 오류 메시지를 생성하여 처리할 수 있도록 설계되었습니다. 이 인터페이스는 커스텀 유효성 검증 로직을 구현하거나, 스프링의 유효성 검증 프레임워크와 통합할 때 사용됩니다.

  • 단순히 Annotation을 선언해주면 검증이 완료되는 이유는 Validator(Validation을 사용하는것)가 존재하기 때문이다. 
  • Spring Boot는 validation 라이브러리를 설정하면 'org.springframework.boot:spring-boot-starter-validation'자동으로 Bean Validator를 Spring에 통합되도록 설정해준다.

동작 순서

  • Spring Boot Application 실행 시 자동으로 Bean Validator가 통합된다.
  1. LocalValidatorFactoryBean 을 Global Validator로 등록한다.
    • Class Diagram

  1. Global Validator가 Default로 적용되어 있으니 @Valid, @Validated 만 적용하면 된다.
  2. Bean Validation Annotation이 있으면 검증을 수행한다.     ex) @NotNull, @NotBlank, @Max 등등..
  3. Validation Error가 발생하면 FieldError, ObjectError를 생성하여 BindingResult에 담아준다.

 

@Valid, @Validated 차이점

  1. @Valid 는 JAVA 표준이고 @Validated 는 Spring 에서 제공하는 Annotation이다.
  2. @Validated 를 통해 Group Validation 혹은 Controller 이외 계층에서 Validation이 가능하다.
  3. @Valid 는 MethodArgumentNotValidException 예외를 발생시킨다.
  4. @Validated 는 ConstraintViolationException 예외를 발생시킨다.

 

Validator 적용

  • Validator 적용 전
    • @ModelAttribute 각각의 필드 타입에 맞추어 바인딩(변환) 시도
      • 성공 : Controller 정상 호출
      • 실패 : TypeMismatch FieldError 발생
  • Validator 적용 후
    • @ModelAttribute → 각 필드 바인딩 → 성공한 필드만 Bean Validation 적용
      • Integer 타입 필드에 문자가 오면 애초에 검증의 의미가 없다.
      • 성공 : String 필드에 문자입력 → 바인딩 성공 → String 필드에 Bean Validation 적용
      • 실패 : Integer 필드에 문자입력 → 바인딩 실패 → bindingResult에 TypeMismatch FieldError 추가 → 바인딩에 실패한 필드는 값이 없음(null) → Bean Validation 적용하지 않음
Bean Validator는 바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다. 바인딩(변환)에 성공한 필드만이 Bean Validation을 적용하는 의미가 있다.

 

 

 

에러 메세지

📌 Spring의 Bean Validation은 Default로 제공하는 Message들이 존재하고 임의로 수정할 수 있다

 

 

Error Message

  • Bean Validation을 적용하고 BindingResult에 등록된 검증 오류를 확인해보면 오류가 Annotation 이름으로 등록되어 있다.
@Data
public class TestDto {

    @NotBlank
    private String stringField;

    @NotNull
    @Range(min = 1, max = 9999)
    private Integer integerField;

}

@Slf4j
@RestController
public class BeanValidationController {

    @PostMapping("/error-message")
    public String beanValidation(
            @Validated @ModelAttribute TestDto dto,
            BindingResult bindingResult
    ) {
				// bindingResult Field Error 출력
        if (bindingResult.hasErrors()) {
            return String.valueOf(bindingResult.getFieldError());
        }

        // 성공시 문자열 반환
        return "회원가입 성공";
    }

}

 

Postman

 

  • integerField 는 비워두고 stringField 만 값(”문자열”) 입력
  • 출력결과
Field error in object 'testDto' on field 'integerField': rejected value [null]; codes [NotNull.testDto.integerField,NotNull.integerField,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDto.integerField,integerField]; arguments []; default message [integerField]]; default message [널이어서는 안됩니다]

NotNull 오류 코드를 기반으로 MessageCodesResolver 를 통해 메세지 코드 생성

 

Spring 에서는 오류 메시지 코드관리를 위해 MessageCodesResolver 인터페이스의 구현체인 DefaultMessageCodesResolver를 기본으로 사용한다.

 

 

에러 메세지 수정하기

  1. NotNull.Object.fieldName
    • Annotation의 message 속성 사용
@Data
public class TestDto {

	@NotBlank(message = "메세지 수정 가능")
	private String stringField;

}

  1. NotNull.fieldName(MessageSource)
    • 필드명에 맞춘 사용자 정의 Message
  2. NotNull.FieldType(MessageSource)
    • 필드 타입에 맞춘 사용자 정의 Message
  3. NotNull
    • Annotation 자체에 포함된 Default Message
  • 네가지 방법중 Annotation의 message 속성을 사용하여 에러 메세지를 수정하면 됩니다.
MessageSource란 Spring에서 지원하는 인터페이스로 메세지의 국제화를 위해 사용된다.

 

 

Object Error

  • 필드 단위가 아닌 객체 전체에 대한 오류를 나타낸다. 예를들어 두 필드 간의 관계를 검증할 때 ObjectError를 통해 해당 오류를 BindingResult에 기록할 수 있다.
지금까지 위에서 배운 내용은 객체가 아닌 필드 단위를 검증해서 발생하는 Field Error에 대한 내용

 

  • @ScriptAssert
    • 코드예시
      • 비밀번호와 비밀번호 확인 필드가 동일한지 검증
@Data
@ScriptAssert(lang = "javascript", script = "_this.password === _this.confirmPassword", message = "Passwords do not match")
public class UserRegistrationDto {

    @NotBlank(message = "Password is required")
    private String password;

    @NotBlank(message = "Confirm password is required")
    private String confirmPassword;
}
  • 실제로 @ScriptAssert는 제약사항 때문에 사용하지 않는다.
  • 실무에서는 훨씬 복잡한 Validation들이 필요하지만 대응이 불가능하다.
  • ex) 다른 객체끼리의 비교 혹은 DB조회 결과와 비교
  • Object Error의 경우 Java 코드로 직접 Validation 한다.
  • Java 코드로 구현하기
    • 요구사항 : 총 구매 가격이 10000원 이상이여야 한다.
      • price * count ≥ 10000
@Getter
@AllArgsConstructor
public class OrderRequestDto {

    @NotNull
    @Range(min = 1000)
    private Integer price;

    @NotNull
    @Range(min = 1)
    private Integer count;

}

@Slf4j
@RestController
public class BeanValidationController {

    @PostMapping("/object-error")
    public String objectError(
            @Validated @ModelAttribute OrderRequestDto requestDto,
            BindingResult bindingResult
    ) {

        // 합이 10000원 이상인지 확인
        int result = requestDto.getPrice() * requestDto.getCount();
        if (result < 10000) {
            // Object Error
            bindingResult.reject("totalMin", new Object[]{10000, result}, "총 합이 10000 이상이어야 합니다.");
        }
        // Error가 있으면 출력
        if (bindingResult.hasErrors()) {
            log.info("errors={}", bindingResult);
            return bindingResult.getAllErrors().get(0).getDefaultMessage();
        }

        // 성공로직 ...
        return "성공";
    }
    
}
  • Object Error는 로직으로 구현하면 된다.
  • Postman

 

 

 

Bean Validation의 충돌

📌 등록, 수정 API에서 각각 다른 Validation이 적용된다면?

 

요구사항

  • 상품
    • id (식별자)
    • name (이름)
    • price (가격)
    • count (재고)
  • 상품 등록 API
    • 식별자 값은 필수가 아니다.
    • name은 null, “”, “ “을 허용하지 않는다.
    • price는 10 ~ 10000 사이의 숫자로 생성한다.
    • count는 1 ~ 999 사이의 숫자로 생성한다.
  • 상품 수정 API
    • 식별자 값이 필수이다.
    • name은 null, “”, “ “을 허용하지 않는다.
    • price는 무제한으로 허용한다.
    • count는 1 ~ 999 사이의 숫자로 생성한다.
일반적으로 수정 API를 만들 때 식별자 id값을 항상 Controller에서 받도록 구성한다. HTTP 요청은 사용자가 임의로 변경하여 요청할 수 있음으로 항상 서버에서 최종적으로 추가 검증을 진행 해야한다. ex) 게시글 수정시 요청자 본인이 쓴 글인지 확인한다.

 

Product 저장 API

@Data
public class ProductRequestDto {

		// 식별자는 Database에서 자동생성
		
    @NotBlank
    private String name;

    @NotNull
    @Range(min = 10, max = 10000)
    private Integer price;

    @NotNull
    @Range(min = 1, max = 999)
    private Integer count;
}

@Slf4j
@RestController
public class ConflictValidationController {

    @PostMapping("/product")
    public String save(
            @Validated @ModelAttribute ProductRequestDto requestDto
    ) {
				log.info("생성 API가 호출 되었습니다.");
        // Validation 성공시 repository 저장로직 호출
        return "상품 생성이 완료되었습니다";
    }

}

 

Product 수정 API

@Data
public class ProductRequestDto {
	
		@NotBlank
    private String name;

		// price 무제한 요구사항 반영
    @NotNull
    private Integer price;

    @NotNull
    @Range(min = 1, max = 999)
    private Integer count;
}

@Slf4j
@RestController
public class ConflictValidationController {
    @PutMapping("/product/{id}")
    public String update(
            @PathVariable Long id,
            @Validated @ModelAttribute ProductRequestDto test
    ) {
        log.info("수정 API가 호출 되었습니다.");
        // Validation 성공시 repository 수정로직 호출
        return "상품 수정이 완료되었습니다.";
    }
}

@PathVariable의 required 속성의 기본값은 true이다.

 

 

해결방법

  1. 저장할 Object를 직접 사용하지 않고 SaveRequestDto, UpdateRequestDto 따로 사용한다.
  2. Bean Validation의 groups 기능을 사용한다.

 

groups

  • Bean Validation의 groups 속성은 다양한 유효성 검사 시나리오를 정의할 때 사용된다. 동일한 객체에 대한 검증을 상황에 따라 다르게 적용하고 싶을 때 groups를 활용할 수 있다.
// 저장용 group
public interface SaveCheck {

}

// 수정용 group
public interface UpdateCheck {

}

@Data
public class ProductRequestDtoV2 {

    // 저장, 수정 @NotBlank Validation 적용
    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
    private String name;

    // 사용하는 모든곳에서 @NotNull Validation 적용
    @NotNull
    // 저장만 @Range 반영
    @Range(min = 10, max = 10000, groups = SaveCheck.class)
    private Integer price;

    @NotNull
    @Range(min = 1, max = 999)
    private Integer count;

}

@Slf4j
@RestController
public class ProductController {

    @PostMapping("/v2/product")
    public String save(
            // 저장 속성값 설정
            @Validated(SaveCheck.class) @ModelAttribute ProductRequestDtoV2 requestDtoV2
    ) {
        log.info("생성 API가 호출 되었습니다.");
        // Validation 성공시 repository 저장로직 호출
        return "상품 생성이 완료되었습니다";
    }

    @PutMapping("/v2/product/{id}")
    public String update(
            @PathVariable Long id,
            // 수정 속성값 설정
            @Validated(UpdateCheck.class) @ModelAttribute ProductRequestDto test
    ) {
        log.info("수정 API가 호출 되었습니다.");
        // Validation 성공시 repository 수정로직 호출
        return "상품 수정이 완료되었습니다.";
    }

}

 

 

 

 

groups VS DTO 분리

📌 Bean Validation의 충돌이 발생하는 경우 대부분 DTO를 분리하는 방법이 적절하다.

 

  • groups VS DTO 분리
    • groups 속성을 사용하면 등록과 수정시 각각 다르게 Validation이 적용된다.
      • 가독성이 떨어지고 코드 복잡도가 올라간다.
    • 실무에서는 등록 폼과 수정 폼 자체를 분리해서 사용하기 때문에 DTO 분리 방법을 사용하면 된다.
    • 단, 네이밍은 일관성있게 작성해야 한다.(SaveRequestDto, UpdateRequestDto)
  • DTO 분리
    • 실제로 간단한 프로젝트를 개발해보면 저장, 수정시 Request가 비슷한 경우가 있다.
    • 각각의 장단점이 존재하지만 어설프게 하나로 합칠 경우 유지보수시 엄청난 경험을 할 수 있다.
    • RequestDto가 변한다는건 해당 API의 스펙 자체가 변경되어 많은 수정이 발생한다.
    • 실무에서는 거의 발생하지 않는 경우기 때문에 간단한게 아니라면 대부분 분리하도록 하자!
  • @Validated VS @Valid
    1. @Validated

  • 속성값이 존재한다.
  • spring이 제공하는 Annotation
  1. @Valid

  • 속성값이 존재하지 않는다, groups 기능 지원하지 않는다.
  • groups 기능을 사용하려면 @Validated를 사용해야 한다.
  • Java 표준 Annotation

 

 

@ModelAttribute, @RequestBody

📌 @Valid, @Validated@ModelAttribute뿐만 아니라 @RequestBody에도 적용할 수 있다. @ModelAttribute는 요청 파라미터 혹은 Form Data(x-www-urlencoded)를 다룰 때 사용하고 @RequestBody 는 HTTP Body Data를 Object로 변환할 때 사용한다.

 

@RequestBody 적용

@Data
public class ExampleRequestDto {

	@NotBlank
	private String field1;

	@NotNull
	@Range(min = 1, max = 150)
	private Integer field2;

}

@Slf4j
@RestController
public class RequestBodyController {
	
    @PostMapping("/example")
    public Object save(
            @Validated @RequestBody ExampleRequestDto dto,
            BindingResult bindingResult
    ) {
        log.info("RequestBody Controller 호출");

        if(bindingResult.hasErrors()) {
            log.info("validation errors={}", bindingResult);
            // Field, Object Error 모두 JSON으로 반환
            return bindingResult.getAllErrors();
        }

        // 성공 시 RequestDto 반환(의미 없음)
        return dto;
    }

}

 

 

  • Rest API 요청의 세가지 경우의 수
    1. 성공 요청: 성공

  • Controller 정상 호출
  • 응답 반환
  1. 실패 요청: JSON을 객체로 변환하는 것 자체가 실패

  • field2에 String 입력
    • JSON → Object 변환 실패
  • 중요! Controller가 호출되지 않는다.
  • 반드시 JSON → Object로 변환이 되어야 Validation이 진행된다.
  1. 검증 오류 요청: JSON을 객체로 변환하는 것은 성공, 검증에서 실패

  • field2의 값에 범위를 넘어서는 값 입력 @Range(max = 150)
    • bindingResult.getAllErrors() 가 MessageConverter에 의해 JSON으로 변환되어 반환된다.
    • Controller를 실제로 호출한다.
    • log로 작성한 bindingResult error들이 콘솔에 출력된다.

 

정리

  • @ModelAttribute와 @RequestBody 차이점
    1. @ModelAttribute
      • 각각의 필드 단위로 바인딩한다.
      • 특정 필드 바인딩이 실패하여도 나머지 필드는 정상적으로 검증 처리할 수 있다.
      • 특정필드 변환 실패
        • 컨트롤러 호출, 나머지 필드 Validation 적용
    2. @RequestBody
      • 필드별로 적용되는것이 아니라 객체 단위로 적용된다.
      • MessageConverter가 정상적으로 동작하여 Object로 변환하여야 Validation이 동작한다.
      • 특정필드 변환 실패
        • 컨트롤러 미호출, Validation 미적용
  • 추가내용
    • bindingResult.getAllErrors()는 FieldError와 ObjectError 모두 반환한다.
    • Spring은 MessageConverter를 이용해 Error 객체들을 변환하여 응답한다.
    • RequestDTO 의 경우, 생성, 수정, 삭제, 모두 비슷하게 생겼어도 따로 분리해서 사용하자.
    • 작성한 코드는 예시일 뿐 실제로는 API Spec에 맞는 응답을 만들어 클라이언트에 전달 해야한다.
      • @ControllerAdvice

 

의존관계 주입

📌 객체 간의 의존성을 스프링 컨테이너가 자동으로 관리하고 주입해주는 설계 패턴입니다. 객체가 다른 객체를 필요로 할 때, 직접 생성하지 않고 외부에서 주입받아 사용하도록 설계하는 방식입니다.

  • @Autowired 는 의존성을 자동으로 주입할 때 사용하는 Annotation 이다.
    • 기본적으로 주입할 대상이 없으면 오류가 발생한다.(required = true)

 

의존관계 주입의 기본 개념

  1. 의존성(Dependency):
    • A 객체가 B 객체를 사용해야 한다면, A는 B에 의존하고 있다고 말합니다.
    • 이때 B 객체를 A 내부에서 직접 생성하지 않고, 외부에서 주입받는 것을 의존관계 주입이라고 합니다.
  2. 스프링 DI:
    • 스프링 컨테이너가 객체(빈)를 생성하고 관리하면서 필요한 의존성을 자동으로 주입합니다.
    • 개발자는 직접 의존성을 생성하거나 연결할 필요가 없으며, 컨테이너가 이를 처리합니다.

 

왜 의존성 주입인가?

@Autowired와 스프링 컨테이너가 있기 때문에, 개발자가 의존 객체를 직접 생성하지 않아도 되고, 컨테이너가 이를 대신 처리합니다. 이 점이 의존성 주입의 핵심입니다.

장점

  1. 결합도 감소:
    • MyApp은 MyService의 구체적인 구현체를 몰라도 됩니다. (DIP 원칙 준수)
    • 다른 구현체로 변경할 때 코드 수정이 필요 없습니다.
  2. 유연성 증가:
    • 스프링 컨테이너에서 주입받는 객체를 쉽게 교체하거나 확장할 수 있습니다.
  3. 테스트 용이성:
    • 테스트 환경에서 Mock 객체를 주입할 수 있습니다.

 

1. 생성자 주입

  • 생성자를 통해 의존성을 주입하는 방법.
  • 최초에 한번 생성된 후 값이 수정되지 못한다.[불변, 필수]
public interface MyService {
    void doSomething();
}

// Spring Bean으로 등록
@Service
public class MyServiceImpl implements MyService {
    @Override
    public void doSomething() {
        System.out.println("MyServiceImpl 메서드 호출");
    }
}

// 생성자 주입 방식
@Component
public class MyApp {
		// 필드에 final 키워드 필수! 무조건 값이 있도록 만들어준다.(필수)
    private final MyService myService;

		// 생성자를 통해 의존성 주입, 생략 가능
    @Autowired
    public MyApp(MyService myService) {
        this.myService = myService;
    }

    public void run() {
        myService.doSomething();
    }
    
}

@ComponentScan(basePackages = "com.example.springdependency.test")
public class Main {
    public static void main(String[] args) {

        ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);

        // 등록된 MyApp 빈 가져오기
        MyApp myApp = context.getBean(MyApp.class);

        // 빈 메서드 호출
        myApp.run();
    }
}

-------------------------------------

// 생성자가 두개인 경우 생략이 불가능하다.
@Component
public class MyApp {
		// 필드에 final 키워드 필수! 무조건 값이 있도록 만들어준다.(필수)
    private final MyService myService;
    
    public MyApp(MyService myService, String myRepository) {
        this.myService = myService;
    }

		// 생성자를 통해 의존성 주입
    // @Autowired를 생략하기 위해서는 생성자가 하나여야 한다.
    public MyApp(MyService myService) {
        this.myService = myService;
    }

    public void run() {
        service.doSomething();
    }
    
}
  • 생성자가 하나인 경우 @Autowired 생략이 가능하다.
  • 둘중 어떤 생성자를 사용해야 하는지 Spring은 알지 못한다.

 

 

2. Setter 주입

  • Setter 메서드를 통해 의존성을 주입하는 방법.
@Component
public class MyApp {

    private MyService myService;

    // Setter 주입
    @Autowired
    public void setMyService(MyService myService) {
        this.myService = myService;
    }
    
    public void run() {
        myService.doSomething();
    }

}

선택하거나, 변경 가능한 의존관계에 사용한다.(생성자 주입은 필수 값)

// MyService가 Spring Bean으로 등록되지 않은 경우에도 주입이 가능하다.
@Autowired(required = false)
public void setMyService(MyService myService) {
    this.myService = myService;
}

// 실행 도중 인스턴스를 바꾸고자 하는 경우
// setMyService(); 메서드를 외부에서 호출하면 된다.(이런 경우는 거의 없음)

 

 

3. 필드 주입

  • 필드에 직접적으로 주입하는 방법 (가장 추천되지 않음).
@Component
public class MyApp {

    @Autowired
    private MyService myService;  // 필드에 직접 주입

    public void run() {
        myService.doSomething();
    }
    
}
  • 코드는 간결하지만 Spring이 없으면 사용할 수 없다.
    • 사용하지 않아야 한다.
// Spring을 사용하지 않는 경우 실행이 불가능하다.
public class MainV2 {
    public static void main(String[] args) {
        MyApp myApp = new MyApp();
        myApp.run();
    }
}
  • 외부에서 myService 값을 설정하거나 변경할 방법이 없다.
    • 결국 setter를 만들어야 한다.
  • 순수 Java 코드로 사용할 수 없다. = 테스트 코드 작성이 힘들다.
  • Application의 실행과 관계 없는 @SpringBootTest 테스트 코드나 Spring에서만 사용하는 @Configuration 같은 곳에서 주입할 때 주로 사용한다.

 

4. 일반 메서드 주입

  • 생성자, setter 주입으로 대체가 가능하기 때문에 사용하지 않는다.
@Component
public class MyApp {

    private MyService myService;

    // 일반 메서드 주입
    @Autowired
    public void init(MyService myService) {
        this.myService = myService;
    }
    
    public void run() {
        myService.doSomething();
    }

}

 

의존관계를 자동으로 주입할 객체가 Spring Bean으로 등록되어 있어야 @Autowired 로 주입이 가능하다.

 

 

생성자 주입

📌 과거 setter, 필드 주입도 사용했지만 현재는 DI를 가지고 있는 대부분의 Framework가 생성자 주입 방식을 권장한다.

 

생성자 주입을 선택하는 이유

  • 불변(immutable)
    • 어떤 요리(Web Application)를 만들지 정해졌다면 이미 재료(Bean)와 의존 관계가 결정된다.
    • 객체를 생성할 때 최초 한번만 호출된다.(불변)
    • setter 주입을 사용하면 접근제어자가 public 으로 설정되어 누구나 수정할 수 있게된다.
  • 실수 방지
    • 순수 Java 코드로 사용할 때(주로 테스트 코드) 생성자의 필드를 필수로 입력하도록 만들어준다.(NPE 방지)
    • 컴파일 시점에 오류를 발생 시킨다. 즉, 실행 전에 오류를 알 수 있다.
public class MyApp {
    private MyService myService;

    public MyApp() {
        this.myService = new MyService(); // 직접 생성
    }

    public void run() {
        myService.doSomething();
    }
}

---------------------------------------

@Component // 해당 어노테이션이 MyService 객체를 bean으로 만들어준다. 즉 스프링이 객체 MyService를 저장한다.
public class MyApp {
    private final MyService myService;

    @Autowired
    public MyApp(MyService myService) {
        this.myService = myService; // 외부에서 주입 (직접 선언이 아니라 스프링에 저장된 객체 사용)
    }

    public void run() {
        myService.doSomething();
    }
}

 

  • 위 코드 참고
  • @Autowired: 스프링 컨테이너가 MyService 타입의 빈을 찾아서 자동으로 주입합니다.
  • MyApp은 MyService를 직접 생성하지 않습니다. 대신, 스프링 컨테이너가 제공한 인스턴스를 사용합니다.

 

 

 

Spring Framework에 의존하지 않아도 객체 지향 특성을 가장 잘 사용하는 방법이다.

필드에 final 은 생성자 주입 방식만 사용할 수 있다. 나머지 주입 방식들은 모두 생성 이후에 호출되어 사용할 수 없다.

 

 

@RequiredArgsConstructor

📌 실제 Web Application을 개발하면 대부분이 불변 객체이고 생성자 주입 방식을 선택하게 된다. 이런 반복되는 코드를 편안하게 작성하기 위해 Lombok에서 제공하는 Annotation 이다.

 

@RequiredArgsConstructor

  • final 필드를 모아서 생성자를 자동으로 만들어 주는 역할
  • Annotation Processor 가 동작하며 컴파일 시점에 자동으로 생성자 코드를 만들어준다.
  • 사용 방법
@Component
@RequiredArgsConstructor
public class MyApp {
		// 필드에 final 키워드 필수! 무조건 값이 있도록 만들어준다.(필수)
    private final MyService myService;
    
    // Annotation Processor가 만들어 주는 코드
    // public MyApp(MyService myService) {
    //     this.myService = myService;
    // }

    public void run() {
        myService.doSomething();
    }
    
}
  • 만약 생성자가 필요한 경우가 생긴다면, 생성자 주입 방식을 직접 선언하면 된다.
생성자를 하나 만들고 @Autowired 를 사용한 코드와 똑같이 동작한다.

'Back-End (Web) > Spring' 카테고리의 다른 글

ArgumentResolver  (0) 2025.01.08
[Spring] 스프링 정리  (0) 2025.01.07
[Spring] Bean 등록  (0) 2024.12.25
[Spring] Spring의 핵심 개념  (0) 2024.12.24
[Spring] Layered Architecture  (2) 2024.12.20

@ComponentScan

📌 Spring이 특정 패키지 내에서 @Component, @Service, @Repository, @Controller 같은 Annotation이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록하는 기능이다. 개발자가 Bean을 직접 등록하지 않고도 Spring이 자동으로 관리할 객체들을 찾는다.

 

ComponentScan의 역할

  • Chef가 요리할 재료를 자동으로 식료품 저장고에서 찾아오는 과정, Chef는 스스로 필요한 재료를 찾아 요리에 사용한다.

  • 요리사(개발자)가 직접 재료(Bean)를 찾아서 가져올 필요가 없다.

 

@ComponentScan

  1. 특정 패키지 내에 @Component Annotation이 붙은 클래스를 자동으로 찾아서 Spring Bean으로 등록한다.
    • Annotation을 이용해 Bean을 등록할 수 있어 코드가 간결해지고 유지보수가 쉬워진다.
  2. 스캐닝 범위는 주로 애플리케이션의 루트(최상위) 패키지에서 시작된다.
  3. @SpringBootApplication
    • 스프링 부트 애플리케이션의 시작점을 정의하기 위해 사용하는 애너테이션입니다. 이 애너테이션은 여러 기능을 결합한 복합 애너테이션으로, 스프링 부트 애플리케이션을 간단히 설정하고 실행할 수 있도록 돕습니다.
    • SpringBoot로 프로젝트를 생성하면 main() 메서드가 있는 클래스 상단에 @SpringBootApplication Annotation 이 존재한다.

 

 

  • @ComponentScan의 속성
    • basePackages: 특정 패키지를 스캔할 때 사용, 배열로 여러개를 선언할 수 있다.
      • 예시: @ComponentScan(basePackages = {"com.example", "com.another"})
    • basePackageClasses: 특정 클래스가 속한 패키지를 기준으로 스캔할 수 있다.
      • 예시: @ComponentScan(basePackageClasses = MyApp.class)
    • excludeFilters: 스캔에서 제외할 클래스를 필터링할 수 있다.
      • 예시: @ComponentScan(excludeFilters = @ComponentScan.Filter(SomeClass.class))
    • includeFilters: 특정 조건에 맞는 클래스만 스캔하여 포함할 수 있다.
      • 예시: @ComponentScan(includeFilters = @ComponentScan.Filter(Service.class))

 

@ComponentScan의 동작 순서

1. 스프링 컨테이너가 빈 등록

  • Spring Application이 실행되면 @ComponentScan이 지정된 패키지를 탐색한다.
  • @ComponentScan은 @Component, @Service, @Repository, @Controller 등의 애너테이션이 붙은 클래스를 탐색합니다.
  • 해당 클래스들을 스프링 빈으로 등록합니다.
  • 구현 클래스뿐만 아니라, 인터페이스와 그 구현체도 함께 빈으로 등록됩니다.

2. 의존성 정의 (인터페이스 기반 설계)

  • 일반적으로 **인터페이스(추상화)**를 의존성으로 정의합니다.
    구현체는 스프링이 자동으로 선택하고 주입합니다.

3. 구현체 자동 연결

  • 스프링은 등록된 빈 중에서 의존성으로 선언된 인터페이스에 맞는 구현체를 자동으로 찾아 주입합니다.
  • 이 과정은 타입 매칭을 통해 이루어집니다.

 

 

 

@Configuration, @Bean

📌 Spring Bean을 등록하는 방법에는 수동, 자동 두가지가 존재한다.

 

Spring Bean 등록 방법

  • Spring Bean은 Bean의 이름으로 등록된다.
    • 1. 자동 Bean 등록(@ComponentScan, @Component)
      • @Component 이 있는 클래스의 앞글자만 소문자로 변경하여 Bean 이름으로 등록한다.
// myService 라는 이름의 Spring Bean
@Component
public class MyService {

    public void doSomething() {
        System.out.println("Spring Bean 으로 동작");
    }
    
}
  • @ComponentScan 을 통해 @Component로 설정된 클래스를 찾는다.

 

  • 2. 수동 Bean 등록(@Configuration, @Bean)
    • @Configuration 이 있는 클래스를 Bean으로 등록하고 해당 클래스를 파싱해서 @Bean 이 있는 메서드를 찾아 Bean을 생성한다. 이때 해당 메서드의 이름으로 Bean의 이름이 설정된다.
// 인터페이스
public interface TestService {
    void doSomething();
}

// 인터페이스 구현체
public class TestServiceImpl implements TestService {
    @Override
    public void doSomething() {
        System.out.println("Test Service 메서드 호출");
    }
}

// 수동으로 빈 등록
@Configuration
public class AppConfig {
    
    // TestService 타입의 Spring Bean 등록
    @Bean
    public TestService testService() {
        // TestServiceImpl을 Bean으로 등록
        return new TestServiceImpl();
    }
    
}

// Spring Bean으로 등록이 되었는지 확인
public class MainApp {
    public static void main(String[] args) {
        // Spring ApplicationContext 생성 및 설정 클래스(AppConfig) 등록
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        // 등록된 TestService 빈 가져오기
        TestService service = context.getBean(TestService.class);

        // 빈 메서드 호출
        service.doSomething();
    }
}

 

수동으로 Bean을 등록할 때는 항상 @Configuration과 함께 사용해야 Bean이 싱글톤으로 관리된다. CGLIB 라이브러리와 연관이 있다.
더보기

@Configuration과 싱글톤 관리

  • **@Configuration**은 스프링에서 Java Config 클래스를 정의할 때 사용하는 애너테이션입니다. 이 애너테이션을 붙인 클래스는 스프링 컨테이너가 관리하는 설정 클래스로 동작하며, 해당 클래스에 정의된 Bean은 싱글톤으로 관리됩니다.
  • 싱글톤 관리:
    • @Configuration이 붙은 클래스는 내부적으로 CGLIB 동적 프록시 객체로 변환됩니다.
    • 이 프록시 객체는 @Bean 메서드 호출 시, 이미 생성된 Bean이 있으면 이를 반환하고, 없으면 새로운 Bean을 생성합니다.
    • 이를 통해 동일한 Bean이 여러 번 생성되지 않도록 보장합니다.

 

 

Bean 충돌

📌 Bean 등록 방법에는 수동, 자동 두가지가 존재하고 Bean은 각각의 이름으로 생성된다. 이때 이름이 같은 Bean이 설정되고자 한다면 충돌이 발생한다.

 

같은 이름의 Bean 등록

  • 자동 Bean 등록 VS 자동 Bean 등록
public interface ConflictService {
    void test();
}

// Bean의 이름을 service로 설정
@Component("service")
public class ConflictServiceV1 implements ConflictService {
    @Override
    public void test() {
        System.out.println("Conflict V1");
    }
}

// Bean의 이름을 service로 설정
@Component("service")
public class ConflictServiceV2 implements ConflictService {
    @Override
    public void test() {
        System.out.println("Conflict V2");
    }
}

// componentScan의 범위를 conflict 패키지 하위로 설정
@ComponentScan(basePackages = "com.example.springconcept.conflict")
public class ConflictApp {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ConflictApp.class);

        // Service 빈을 가져와서 실행
        ConflictService service = context.getBean(ConflictService.class);

        service.test();
    }
}
  • ConflictingBeanDefinitionException 발생

 

수동 Bean 등록 VS 자동 Bean 등록

// conflictService 이름으로 Bean 생성
@Component
public class ConflictService implements MyService {
    @Override
    public void doSomething() {
        System.out.println("ConflictService 메서드 호출");
    }
}

public class ConflictServiceV2 implements MyService {
    @Override
    public void doSomething() {
        System.out.println("ConflictServiceV2 메서드 호출");
    }
}

// 수동으로 Bean 등록
@Configuration
public class ConflictAppConfig {
		
		// conflictService 이름으로 Bean 생성
    @Bean(name = "conflictService")
    MyService myService() {
        return new ConflictServiceV2();
    }

}

@ComponentScan(basePackages = "com.example.springconcept.conflict2")
public class ConflictApp2 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ConflictApp2.class);

        // Service 빈을 가져와서 실행
        MyService service = context.getBean(MyService.class);

        service.doSomething();
    }
}

 

  • 수동 Bean 등록이 자동 Bean 등록을 오버라이딩해서 우선권을 가진다.
  • 의도한 결과라면 다행이지만, 아닌 경우(실수)가 대부분이다. → 버그 발생
  • Spring Boot에서는 수동과 자동 Bean등록의 충돌이 발생하면 오류가 발생한다.

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

 

설정 변경(application.properties)

// 수동, 자동 Bean을 동시에 등록할 때 이름이 같으면 수동 Bean이 오버라이딩
spring.main.allow-bean-definition-overriding=true 

// 기본값
spring.main.allow-bean-definition-overriding=false

 

 

@Qualifier, @Primary

📌 같은 타입의 Bean이 중복된 경우 해결하기 위해 사용하는 Annotation

  • 같은 타입의 Bean 충돌 해결 방법
    1. @Autowired + 필드명 사용
      • @Autowired 는 타입으로 먼저 주입을 시도하고 같은 타입의 Bean이 여러개라면 필드 이름 혹은 파라미터 이름으로 매칭한다.
public interface MyService { ... }

@Component
public class MyServiceImplV1 implements MyService { ... }

@Component
public class MyServiceImplV2 implements MyService { ... }

@Component
public class ConflictApp {

	// 필드명을 Bean 이름으로 설정
	@Autowired
	private MyService myServiceImplV2;
	...
}
  1. @Qualifier 사용
    • Bean 등록 시 추가 구분자를 붙여 준다.
    • 생성자 주입, setter 주입 사용 가능
@Component
@Qualifier("firstService")
public class MyServiceImplV1 implements MyService { ... }

@Component
@Qualifier("secondService")
public class MyServiceImplV2 implements MyService { ... }

@Component
public class ConflictApp {

		private MyService myService;

		// 생성자 주입에 구분자 추가
		@Autowired
		public ConflictApp(@Qualifier("firstService") MyService myService) {
				this.myService = myService;
		}
	
		// setter 주입에 구분자 추가
		@Autowired
		public void setMyService(@Qualifier("firstService") MyService myService) {
				this.myService = myService;
		}
	...
}
  1. @Primary 사용
    • @Primary로 지정된 Bean이 우선 순위를 가진다.
@Component
public class MyServiceImplV1 implements MyService { ... }

@Component
@Primary
public class MyServiceImplV2 implements MyService { ... }

@Component
public class ConflictApp {

		private MyService myService;

		@Autowired
		public ConflictApp(MyService myService) {
				this.myService = myService;
		}
	...
}
  • 실제 적용 사례
    • Database가 (메인 MySQL, 보조 Oracle) 두개 존재하는 경우
      • 기본적으로 MySQL을 사용할 때 @Primary를 사용하면 된다.
      • 필요할 때 @Qualifier로 Oracle을 사용하도록 만들 수 있다.
      • 동시에 사용되는 경우 @Qualifier 의 우선순위가 높다.
같은 타입의 Bean이 여러개 조회되었지만 모든 Bean이 필요하다면, Java의 자료구조 Map, List를 사용하는 방법도 있다.

 

 

수동 VS 자동

📌 Annotation 기반의 Spring에서는 자동 Bean 등록과 의존관계 주입을 사용하는 경우를 주로 사용한다. @Component 뿐만 아니라 @Controller, @Service, @Repository 등 자동으로 쉽게 등록할 수 있는 Annotation들을 지원하고 Spring Boot는 ComponentScan 방식을 기본으로 사용한다.

 

  • 자동 Bean 등록을 사용하는 이유
    1. 다양한 Annotation으로 편리하게 등록할 수 있다.
    2. Spring Boot는 ComponentScan 방식을 기본으로 사용한다.
    3. 간단하지만 OCP, DIP를 준수하며 개발할 수 있다.
  • 수동 Bean 등록을 사용하는 경우
    1. 외부 라이브러리나 객체를 Spring Bean으로 등록할 때
      • 외부 라이브러리에서 제공하는 클래스는 자동 등록이 불가능하다.
    2. 데이터베이스 연결과 같이 비지니스 로직을 지원하는 기술들에 사용한다.
      • 비지니스 로직보다 그 수가 아주 적지만 Application에 광범위하게 적용된다.
      • 설정 정보에 명시되어 있어서 유지보수성이 증가한다.
    3. 같은 타입의 Bean 여러개 중 하나를 명시적으로 선택해야 할 때

꼭 필요한 경우가 아니라면 자동 Bean 등록을 사용하면 된다.

 

'Back-End (Web) > Spring' 카테고리의 다른 글

[Spring] 스프링 정리  (0) 2025.01.07
[Spring] 의존관계 주입  (0) 2024.12.26
[Spring] Spring의 핵심 개념  (0) 2024.12.24
[Spring] Layered Architecture  (2) 2024.12.20
[Spring] Server에서 Client로 Data를 전달하는 방법  (0) 2024.12.19

Spring Container

📌 Spring으로 구성된 애플리케이션에서 객체(Bean)를 생성, 관리, 소멸하는 역할을 담당한다. 애플리케이션 시작 시, 설정 파일이나 Annotation을 읽어 Bean을 생성하고 주입하는 모든 과정을 컨트롤한다. 심지어는 의존성마저 주입한다.

  • 총괄주방장 = shef 라고 보면 편하다.

  • Spring Container를 사용하면 인터페이스에만 의존하는 설계가 가능해진다.
    • OCP, DIP 준수

 

Spring Container의 종류

  • BeanFactory
    • Spring Container의 최상위 인터페이스
    • Spring Bean을 관리하고 조회한다.
  • ApplicationContext
    • BeanFactory의 확장된 형태(implements) -> 진화된 버전
    • Application 개발에 필요한 다양한 기능을 추가적으로 제공한다.
      • 국제화, 환경변수 분리, 이벤트, 리소스 조회
일반적으로 ApplicationContext를 사용하기 때문에 ApplicationContext를 Spring Container라 표현한다.

 

Spring Bean

📌 Spring 컨테이너가 관리하는 객체를 의미한다. 자바 객체 자체는 특별하지 않지만, Spring이 이 객체를 관리하는 순간부터 Bean이 된다. Spring은 Bean을 생성, 초기화, 의존성 주입 등을 통해 관리한다.

  • 모든 객체가 Bean인게 아니라 spring container가 관리하는 객체를 bean이라 하는 것
  • Bean은 new 키워드 대신 사용하는 것이다.
  • Spring Container가 제어한다.

 

Spring Bean의 역할

  • Chef인 Spring Container가 요리할 음식에 사용될 재료(Bean)
    • 요리(Application)의 핵심을 이루는 재료(Bean)

 

  • Spring Bean의 특징
    1. Spring 컨테이너에 의해 생성되고 관리된다.
    2. 기본적으로 Singleton( 애플리케이션 전역에서 단 하나의 인스턴스만 생성하도록 보장하는 디자인 패턴 )으로 설정된다.
    3. 의존성 주입(DI)을 통해 다른 객체들과 의존 관계를 맺을 수 있다.
    4. 생성, 초기화, 사용, 소멸의 생명주기를 가진다.

 

Bean 등록 방법

  • XML, Java Annotation, Java 설정파일 등을 통해 Bean으로 등록할 수 있다.
    • XML
<beans>
    <!-- myBean이라는 이름의 Bean 정의 -->
    <bean id="myBean" class="com.example.MyBean" />
</beans>

--------------------------------------------------------------------

public class MyApp {
    public static void main(String[] args) {
        // Spring 컨테이너에서 Bean을 가져옴
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        MyService myService = context.getBean("myService", MyService.class);
        myService.doSomething();
    }
}
  • Annotation
    • @ComponentScan
    • 개발자가 일일이 빈(Bean)을 설정하지 않아도, 지정한 패키지에서 필요한 클래스들을 자동으로 찾아서 애플리케이션 컨텍스트에 빈(Bean) 등록해 줍니다.
// 이 클래스를 Bean으로 등록
// @Controller, @Service, @Repository
@Component
public class MyService {

    public void doSomething() {
        System.out.println("Spring Bean 으로 동작");
    }
    
}

------------------------------------

@Component
public class MyApp {

    private final MyService myService;

    @Autowired // 의존성 자동 주입
    public MyApp(MyService myService) {
        this.myService = myService;
    }

    public void run() {
        myService.doSomething();
    }
}

---------------------------------------------

// com.example 패키지를 스캔하여 Bean 등록
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        MyApp app = context.getBean(MyApp.class);
        app.run();
    }
    
}

Java 설정파일

@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyService();
    }
}

------------------------------------

public class MyApp {
    public static void main(String[] args) {
        // Spring 컨테이너에서 Bean을 가져옴
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        MyService myService = context.getBean(MyService.class);
        myService.doSomething();
    }
}

 

 

 

IOC(제어의 역전, Inversion Of Control)

📌 객체의 생성과 관리 권한을 개발자가 아닌 Spring 컨테이너가 담당하는 것을 말한다. 기본적으로 개발자가 객체를 직접 생성하고 관리했지만, Spring에서는 컨테이너가 객체 생성, 주입, 소멸을 관리한다.

  • 요리사(개발자)는 필요한 재료를 직접 준비하지 않고, Chef가 알아서 필요한 재료(Bean)을 관리하고 요리사에게 가져다준다.

 

IoC 개념

  1. 객체의 생성 및 생명주기 관리를 개발자가 직접 하는 것이 아니라 컨테이너가 담당한다.
  2. 객체 간의 결합도를 낮춰 유연한 코드가 된다.

 

DI(의존성 주입, Dependency Injection)

📌 Spring이 객체 간의 의존성을 자동으로 주입해주는 것을 의미한다. 한 객체가 다른 객체를 사용할 때, 해당 객체를 직접 생성하지 않고 Spring이 주입해주는 방식이다. IOC를 구현하는 방식 중 하나이다.

  • 셰프가 요리를 만들 때 필요한 재료(Bean)를 자동으로 요리사에게 가져다주는 과정
  • 요리사(개발자)는 재료를 찾을 필요 없이, Chef가 알아서 제공해준다.

 

 

의존성 = 추상화가 아닌 이유

의존성이란, 한 객체가 다른 객체를 사용하거나 그 객체의 기능에 의존하는 관계를 말합니다. 그리고 추상화는 의존성을 설계하는 중요한 방식 중 하나입니다. 하지만 의존성이 항상 추상화와 동일한 개념은 아닙니다. 의존성과 추상화의 관계를 설명하겠습니다.

 

  1. 의존성은 설계의 결과, 추상화는 설계의 방법:
    • 의존성은 클래스 간의 관계를 나타냅니다.
    • 추상화는 의존성을 관리하거나 줄이기 위한 설계 기법입니다.
  2. 구체적인 의존성도 존재:
    • 의존성이 항상 추상화된 타입에만 연결되지 않습니다.
    • 구체 클래스에 대한 의존성도 의존성의 한 형태입니다.

 

IOC, DI 주입 개발자, SPRING 비교 

// Service 인터페이스
public interface MyService {
    void doSomething();
}

// Repository 인터페이스
public interface MyRepository {
    void queryDatabase();
}

// Service 구현체
public class MyServiceImpl implements MyService {
    private MyRepository myRepository;

    // 의존성 주입
    public MyServiceImpl(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    @Override
    public void doSomething() {
        System.out.println("서비스 작업 실행");
        myRepository.queryDatabase();
    }
}

// Repository 구현체
public class MyRepositoryImpl implements MyRepository {
    @Override
    public void queryDatabase() {
        System.out.println("데이터베이스 쿼리 실행");
    }
}

public class MyApp {
    public static void main(String[] args) {
        MyRepository repo = new MyRepositoryImpl();

				// MyRepository repo2 = new MyRepositoryImplV2();

        MyService myService = new MyServiceImpl(repo);
        
				// MyService myService2 = new MyServiceImpl(repo2);

        myService.doSomething();
    }
}

// 새로운 Repository 구현체
public class MyRepositoryImplV2 implements MyRepository {
    @Override
    public void queryDatabase() {
        System.out.println("데이터베이스 쿼리 실행 V2");
    }
}


---------------------------------


// Service 구현체
@Service
public class MyIocService implements MyService {
    
    private final MyRepository myRepository;

    // 생성자 주입(DI 적용)
	@Autowired: 스프링이 자동으로 MyRepository 타입의 빈(Bean)을 찾아 생성자에 주입하도록 지시합니다.
    public MyIocService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    @Override
    public void doSomething() {
        System.out.println("IOC 서비스 작업 실행");
        myRepository.queryDatabase();
    }
}

// Repository 구현체
@Repository
public class MyIocRepository implements MyRepository {

    @Override
    public void queryDatabase() {
        // 데이터베이스와 상호작용
        System.out.println("IOC 데이터베이스 쿼리 실행");
    }
}

// Spring Container 관리(IoC 적용)
@ComponentScan(basePackages = "com.example") 
: @ComponentScan으로 지정된 com.example 패키지를 스캔하여 빈으로 등록할 클래스들을 검색합니다.
public class MyIocApp {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyIocApp.class);

        // Service 빈을 가져와서 실행
        MyService service = context.getBean(MyService.class);
        service.doSomething();
    }
}

// 새로운 Repository 구현체
@Repository
public class MyIocRepositoryV2 implements MyRepository {

    @Override
    public void queryDatabase() {
        // 데이터베이스와 상호작용
        System.out.println("IOC 데이터베이스 쿼리 실행 V2");
    }
}
  • 구현 코드가 변경되어도 클라이언트의 코드에는 영향이 없다.
  • 다른 구현체를 구현하여 Bean으로 등록하면 자유롭게 변경이 가능하다.
    • 위 예시 코드는 @Repository 로 등록된 빈이 중복되어 충돌이 발생한다.
  • 의존성 주입(DI), 제어의 역전(IOC)을 통해 객체 간의 결합도를 낮추고 유연한 설계가 가능해진다.

 

IOC/DI

  1. IoC객체의 제어권을 개발자가 아닌 Spring 컨테이너에게 넘기는 개념으로, Spring이 객체 생성과 관리를 담당한다.
  2. DI는 Spring이 객체 간의 의존성을 자동으로 주입해주는 기법이다.
  3. 의존관계 주입은 객체 간의 결합도를 낮추고 코드의 유연성과 테스트 가능성을 높여준다.

 

 

Singleton Pattern

📌 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴이다.

public class MainApp {
    public static void main(String[] args) {
       // 첫 번째 싱글톤 인스턴스 요청, 구현클래스.getInstance();
        Singleton instance1 = SingletonImpl.getInstance();
        instance1.showMessage(); // 인스턴스 주소값 출력

        // 두 번째 싱글톤 인스턴스 요청, 구현클래스.getInstance();
        Singleton instance2 = SingletonImpl.getInstance();
        instance2.showMessage(); // 인스턴스 주소값 출력
        
        // 다른 구현체로 바꾸려면 DIP, OCP 위반
        Singleton instance3 = SingletonImplV2.getInstance();
        instance3.showMessage();
    }
}
  • 위처럼 인스턴스가 클래스마다 1개씩만 배치된다.
  • 다만 보면 알겠지만, 클래스의 개수가 많아지는 것 같은 느낌이 들 것이다.

 

싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하기 위한 코드의 양이 많다.
  • 구현 클래스에 의존해야 한다.(DIP, OCP 위반)
  • 유연성이 떨어져서 안티패턴으로 불리기도 한다.

 

Spring의 싱글톤 컨테이너

  • Spring은 Web Application을 만들 때 주로 사용된다.

  • Spring Container는 싱글톤 패턴의 문제점들을 해결하면서 객체를 싱글톤으로 관리한다.
  • Spring Bean은 싱글톤으로 관리되는 객체이다.
  • Spring이 Bean을 등록하는 방법은 기본적으로 싱글톤 이다. 하지만, 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.

 

Spring Bean이 싱글톤을 사용하는 이유

더보기

스프링에서 Bean을 기본적으로 싱글톤으로 사용하는 이유는 싱글톤 패턴의 장점을 최대한 활용하면서도, 패턴 자체의 단점들을 해결하기 때문입니다. 이를 단계적으로 설명하겠습니다.


싱글톤 패턴의 장점

  1. 리소스 절약:
    • 객체를 한 번만 생성하고 재사용하므로 메모리와 리소스를 절약할 수 있습니다.
  2. 전역 접근 가능:
    • 애플리케이션 전체에서 동일한 인스턴스를 사용할 수 있어 상태 관리나 공유 자원 관리가 용이합니다.

싱글톤 패턴의 단점과 스프링의 해결책

1. 구현이 복잡하다 (코드가 많다)

  • 문제점:
    • 싱글톤 패턴을 구현하려면 추가적인 코드(정적 변수, 동기화 처리 등)가 필요합니다.
    • 이를 잘못 구현하면 멀티스레드 환경에서 안전하지 않을 수 있습니다.
  • 스프링의 해결:
    • 스프링 컨테이너가 싱글톤 관리를 대신합니다.
    • 개발자는 객체 생성 방식을 신경 쓸 필요 없이, @Component, @Service, @Repository 등의 애너테이션만 추가하면 스프링이 자동으로 관리합니다.

2. 구현 클래스에 의존 (DIP, OCP 위반)

  • 문제점:
    • 싱글톤 패턴은 클래스 자체에서 객체를 관리하기 때문에 구체 클래스에 의존하게 됩니다.
    • 이는 **의존성 역전 원칙(DIP)**과 **개방-폐쇄 원칙(OCP)**을 위반할 가능성을 높입니다.
  • 스프링의 해결:
    • 스프링은 **DI(Dependency Injection)**를 사용하여 의존성을 주입합니다.
    • Bean을 인터페이스 기반 설계로 사용할 수 있게 하여 DIP를 준수합니다.
    • 개발자는 객체 생성 방식을 몰라도 컨테이너가 관리하는 객체를 사용할 수 있으므로 OCP를 준수합니다.

3. 테스트 어려움

  • 문제점:
    • 싱글톤 객체는 전역 상태를 공유하기 때문에 테스트에서 객체의 상태를 초기화하거나 Mock 객체로 대체하기 어렵습니다.
  • 스프링의 해결:
    • 스프링은 DI를 통해 의존성을 주입하므로, 테스트 환경에서 Mock 객체를 쉽게 주입할 수 있습니다.
    • 빈의 스코프를 싱글톤 이외로 설정(예: 프로토타입 스코프)할 수도 있습니다.

4. 유연성 부족 (안티패턴)

  • 문제점:
    • 전역 상태를 공유하는 싱글톤 객체는 설계의 유연성을 저하시켜 애플리케이션의 확장과 변경에 제약을 줄 수 있습니다.
  • 스프링의 해결:
    • 스프링은 필요에 따라 Bean의 **스코프(scope)**를 변경할 수 있습니다:
      • 싱글톤(Singleton): 기본 설정, 모든 요청에서 동일한 인스턴스를 공유.
      • 프로토타입(Prototype): 요청 시마다 새로운 인스턴스 생성.
      • 요청(Request): HTTP 요청마다 새로운 인스턴스 생성.
      • 세션(Session): HTTP 세션마다 새로운 인스턴스 생성.
      • 웹 소켓(WebSocket): 웹소켓 연결마다 새로운 인스턴스 생성.

스프링이 싱글톤을 사용하는 이유

스프링은 싱글톤 패턴의 단점을 해결하면서도, 다음과 같은 이유로 싱글톤을 기본으로 사용합니다:

  1. 효율성:
    • 애플리케이션에서 동일한 빈을 여러 번 생성하지 않고, 한 번 생성된 객체를 재사용하여 리소스를 절약합니다.
  2. 글로벌 상태 관리:
    • 데이터베이스 연결, 캐시, 설정 정보 등 전역적으로 공유할 필요가 있는 객체를 관리하기 적합합니다.
  3. 유연성:
    • 스프링은 개발자가 직접 싱글톤 패턴을 구현하지 않아도, 컨테이너가 이를 관리합니다.
    • 필요에 따라 스코프를 변경하거나 테스트 시 Mock 객체로 대체할 수 있습니다.
  4. 단순화된 코드:
    • 개발자는 @Component, @Service 등 애너테이션만으로 빈을 정의하고 사용할 수 있습니다. 싱글톤 관리 로직을 작성할 필요가 없습니다.

결론

스프링은 싱글톤 패턴의 장점을 최대한 활용하면서, **DI(의존성 주입)**와 유연한 스코프 관리를 통해 단점들을 극복했습니다. 이로 인해 스프링의 Bean은 싱글톤을 기본으로 사용하면서도 효율적이고 확장 가능한 구조를 제공합니다.

 

 

Singleton Pattern의 주의

📌 객체의 인스턴스를 하나만 생성하여 공유하는 싱글톤 패턴의 객체는 상태를 유지(stateful)하면 안된다.

  • 지역 변수라면 변수여도 된다. 하지만 요청마다 값이 변경되거나 외부에서 변경 가능한 데이터가 싱글톤 객체에 저장되면 문제가 생길 수 있다.
  • 공유 객체( 여러 사용자나 스레드가 동시에 접근하여 값을 읽거나 수정할 수 있는 데이터 )는 안된다.

 

상태 유지(stateful)의 문제점

  • 데이터의 불일치나 동시성 문제가 발생할 수 있다.
  • 코드예시
public class StatefulSingleton {
    private static StatefulSingleton instance;
    
    // 상태를 나타내는 필드
    private int value;

    // private 생성자
    private StatefulSingleton() {}

    // 싱글톤 인스턴스를 반환하는 메서드
    public static StatefulSingleton getInstance() {
        if (instance == null) {
            instance = new StatefulSingleton();
        }
        return instance;
    }

    // 상태 변경 메서드
    public void setValue(int value) {
        this.value = value;
    }

    // 상태를 반환하는 메서드
    public int getValue() {
        return this.value;
    }
}
public class MainApp {
    public static void main(String[] args) {
        // 클라이언트 1: 싱글톤 인스턴스를 가져와서 상태를 설정
        StatefulSingleton client1 = StatefulSingleton.getInstance();
        client1.setValue(42);
        System.out.println("클라이언트 1이 설정한 값: " + client1.getValue());

        // 클라이언트 2: 동일한 싱글톤 인스턴스를 사용해 상태를 변경
        StatefulSingleton client2 = StatefulSingleton.getInstance();
        client2.setValue(100);
        System.out.println("클라이언트 2가 설정한 값: " + client2.getValue());

        // 클라이언트 1이 다시 값을 확인
        System.out.println("클라이언트 1이 다시 확인한 값: " + client1.getValue());
    }
}
클라이언트 1이 설정한 값: 42
클라이언트 2가 설정한 값: 100
클라이언트 1이 다시 확인한 값: 100
  • value 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.
  • Spring Bean은 항상 무상태(stateless)로 설계를 해야한다. 아주 중요!
    • 특정 클라이언트에 의존적인 필드가 있거나 변경할 수 있으면 안된다.

 

'Back-End (Web) > Spring' 카테고리의 다른 글

[Spring] 의존관계 주입  (0) 2024.12.26
[Spring] Bean 등록  (0) 2024.12.25
[Spring] Layered Architecture  (2) 2024.12.20
[Spring] Server에서 Client로 Data를 전달하는 방법  (0) 2024.12.19
[Spring] HTTP Message Body & TEXT  (0) 2024.12.18

JDBC

📌 Java 언어를 사용하여 DB와 상호 작용하기 위한 자바 표준 인터페이스로 데이터베이스 관리 시스템(DBMS)과 통신하여 데이터를 삽입(C), 검색(R) , 수정(U) 및 삭제(D)할 수 있게 해준다.

 

JDBC 구조

JDBC의 주요 특징

  1. 표준 API
    1. 대부분의 RDBMS (관계형 데이터베이스 관리 시스템)에 대한 드라이버가 제공되어 여러 종류의 DB 대해 일관된 방식으로 상호 작용할 수 있다.
    ex) Database 종류가 바뀌어도 쿼리문이 실행된다. MySQL → ORACLE
  2. 데이터베이스 연결
  3. SQL 쿼리 실행
  4. Prepared Statement
  5. 결과 집합 처리(Result Set)
    • 데이터베이스로부터 반환된 결과 집합을 처리할 수 있다.
    ex) 데이터를 조회하고 결과를 Java 객체로 매핑할 수 있다.
  6. 트랜잭션 관리
    • JDBC를 사용하여 데이터베이스 트랜잭션을 시작, 커밋(성공) 또는 롤백(실패)하는 등의 트랜잭션 관리 작업을 수행할 수 있습니다.

 

JDBC의 주요 구성 요소

  1. DriverManager:
    • 데이터베이스 드라이버를 관리하고, 데이터베이스 연결(Connection)을 제공합니다.
  2. Connection:
    • 데이터베이스와 애플리케이션 간의 연결을 나타냅니다.
  3. Statement:
    • SQL 문을 실행하는 데 사용됩니다. (예: Statement, PreparedStatement, CallableStatement)
  4. ResultSet:
    • SQL 쿼리 결과를 저장하고 처리하는 객체.
  5. SQLException:
    • 데이터베이스 작업 중 발생하는 예외를 처리하는 클래스.

 

JDBC 작동 원리

  1. 애플리케이션이 JDBC API를 호출합니다.
  2. DriverManager가 요청을 적절한 데이터베이스 드라이버에 전달합니다.
  3. 데이터베이스 드라이버가 데이터베이스에 연결을 설정합니다.
  4. Connection 객체를 통해 SQL 문을 실행하고 결과를 반환받습니다.

 

 

JDBC를 사용하는 단계

a. 데이터베이스 드라이버 로드

  • 데이터베이스와 상호작용하기 위해 드라이버를 메모리에 로드
Class.forName("com.mysql.cj.jdbc.Driver");

 

b. 데이터베이스 연결

  • DriverManager를 사용하여 데이터베이스에 연결.
Connection connection = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/mydatabase", "username", "password"
);

c. SQL 문 실행

  • Statement 또는 PreparedStatement를 사용하여 SQL 문 실행.
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM users");

d. 결과 처리

  • ResultSet 객체를 사용하여 쿼리 결과를 읽고 처리.
while (resultSet.next()) {
    System.out.println("ID: " + resultSet.getInt("id"));
    System.out.println("Name: " + resultSet.getString("name"));
}

e. 자원 해제

  • 사용한 자원(Connection, Statement, ResultSet)을 닫아야 함.
 
resultSet.close();
statement.close();
connection.close();

 

 

 

Statement VS Prepared Statement

📌 Java에서 데이터베이스에 SQL 쿼리를 실행하기 위한 인터페이스이다. 이들은 데이터베이스와의 통신을 통해 쿼리 결과를 반환하거나 데이터 조작을 수행하는 데 사용됩니다.

 

Statement

  • DB와 연결되어 있는 Connection 객체를 통해 SQL문을 Database에 전달하여 실행하고, 결과를 반환받는 객체
public class StatementExample {
    public static void main(String[] args) {
        try {
						// MySqlDriver 파일을 라이브러리에 추가한다.
						
						// Driver연결
						Class.forName("mysql.jdbc.driver.MySqlDriver");
						
						// Database와 연결(계정 접속)
            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/mydatabase", "username", "password");
            
						// Statement 인스턴스 생성
						Statement statement = connection.createStatement();
            
						// String SQL Query
						
            String query = "SELECT * FROM MEMBER WHERE NAME = 'wonuk'";
            
						// Query 실행 -> 결과는 ResultSet으로 반환된다.
						ResultSet rs = statement.execute(query);
            
            // 결과 처리
						while (rs.next()) {
							// 결과 처리 로직
						}
            
						// 연결을 수동으로 끊어줘야한다.
						rs.close();
            statement.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  • SQL 쿼리를 직접 문자열로 작성하여 데이터베이스에 보내는 방법이다.
  • SQL 쿼리는 실행 전에 문자열 형태로 전달되고, 실행 시점에 데이터베이스에 직접 파싱되고 실행.
  • 매번 실행할 때마다 SQL 문을 다시 파싱하므로 성능에 영향을 미칠 수 있고, 보안 취약점을 가질 수 있습니다.

 

 

Prepared Statement

  • SQL문을 Complie 단계에서 ? 를 사용하여 preCompile 하여 미리 준비해놓고 Query문을 파라미터 바인딩 후 실행하고 결과를 반환받는다. 미리 준비해놓았다(Pre) 중요!
public class PreparedStatementExample {
    public static void main(String[] args) {
        try {
						// [Mysql.jdbc.driver.MysqlDriver] 파일을 라이브러리에 추가한다.
						Class.forName("mysql.jdbc.driver.MysqlDriver");
						
            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/mydatabase", "username", "password");
            String query = "SELECT * FROM employees WHERE department = ?";
            PreparedStatement preparedStatement = connection.prepareStatement(query);
            
            // 값을 설정
            preparedStatement.setString(1, "HR");
             
            ResultSet resultSet = preparedStatement.executeQuery();
            
            while (resultSet.next()) {
                // 결과 처리 코드
            }
            
            resultSet.close();
            preparedStatement.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  • SQL 쿼리를 미리 컴파일하여 데이터베이스에 전송할 때 값만 바뀌는 형태로 전달한다.
  • 쿼리가 한 번 컴파일되면 여러 번 실행할 수 있으며, 성능이 향상되고 보안 측면에서 더 안전한다.
  • 동적인 입력값을 placeholder?로 대체하고 파라미터 바인딩을 통해 쿼리를 삽인한다. 즉, 사용자 입력을 직접적으로 쿼리에 삽입하지 않는다.
  • 이스케이핑 처리를 지원한다. 말그대로 탈출(Escape), 입력값이 자동으로 쿼리에 안전하게 이스케이핑된다. 이스케이핑은 입력 데이터에서 잠재적인 SQL 쿼리 문자열을 무력화한다.

 

 

SQL Injection

📌 악의적인 사용자가 애플리케이션에서 입력 데이터를 이용하여 SQL 쿼리를 조작하고 데이터베이스에 무단 접근하거나 데이터를 변조하는 공격이다.

 

 

SQL Injection 종류

  1. Error Based SQL Injection
    1. Database에 고의적으로 오류를 발생시켜 에러 응답을 통해 DB 구조를 파악하는 방법.
  2. Union Based SQL Injection
    1. 컬럼의 개수와 데이터 형식이 같아야 한다.
    2. DB의 UNION 연산자를 사용하여 쿼리 결과값의 조합을 통해 정보를 조회한다.
  3. Blind Based SQL Injection
  4. Stored Procedure SQL Injection
  5. Time Based SQL Injection
  • SQL Injection 예시

  • Java 코드 예시
// 로그인
public String login(String id, String password) {
		return String query = 
			"SELECT * FROM MEMBER WHERE ID = " + id
			+ "AND PASSWORD = " + password;
}

// 검증
public String login(String id, String password) {

			// 매개변수 id, password 가 문제없는지 검증하는 로직 Escape 등
			
			String query = 
			"SELECT * FROM MEMBER WHERE ID = " + id + "AND PASSWORD = " + password;
			return query;
}
  • 해결방법
    • 클라이언트에게 에러 메세지 노출을 차단한다.
    • 입력값을 검증(Validation)한다.
    • Prepared Statements를 사용한다.

 

XSS(Cross Site Scription)

📌 악성 스크립트를 웹사이트에 주입하는 Code Injection 기법 중 하나. 웹 애플리케이션에서 발생하는 보안 취약점으로, 공격자가 악성 스크립트를 사용자 브라우저에서 실행하도록 유도하는 공격입니다.

  • 이를 통해 공격자는 사용자의 세션 쿠키를 탈취하거나, 악성 코드를 실행해 개인 정보를 노출시키고, 웹사이트의 신뢰성을 훼손할 수 있습니다.

XSS의 동작 원리

  • 공격자가 웹 애플리케이션의 취약점을 이용해 악성 스크립트를 삽입.
  • 사용자가 해당 페이지를 열거나 입력값을 처리할 때 악성 스크립트가 실행됨.
  • 브라우저는 이 스크립트를 신뢰된 사이트의 일부로 간주하여 실행.

 

XSS의 위험성

  1. 세션 탈취:
    • 사용자의 세션 쿠키를 탈취하여 계정에 접근.
  2. 웹사이트 변조:
    • 사용자 브라우저에서 악성 콘텐츠를 표시하거나, 위조된 페이지를 로드.
  3. 피싱 공격:
    • 사용자에게 신뢰할 수 없는 링크를 클릭하도록 유도.
  4. 브라우저 악성 코드 실행:
    • 브라우저를 통해 악성 소프트웨어를 배포.

 

XSS의 종류

  • Stored XSS
    • 공격자가 취약점이 있는 Web Application에 악성 스크립트를 영구적으로 저장하여 다른 사용자에게 전달하는 방식
    ex) 게시판(HTML) 글 작성
<script>alert(document.cookie);</script>
  • 해당 script를 게시글에 삽입하면 HTML로 구성되어 있기 때문에 해당 스크립트가 조회하는 사용자에게 실행된다.
  • Reflected XSS
    • 공격 스크립트가 서버에 저장되지 않고 즉시 반사되어 실행됨.
    • URL에 포함된 파라미터나 입력값을 통해 전달.
    • 외부 링크 페이지로 이동시킨다.
    ex) 메일 내 첨부된 링크 클릭 → 가짜 사이트로 연결
  • DOM based XSS
    • 서버와 상관없이 클라이언트 측 JavaScript에서 발생.
    • DOM(Document Object Model)을 조작하여 악성 스크립트를 실행.
  • 해결방법
    1. 입/출력 값을 검증(Validation)하고 필터링하여 해결한다.
    2. 외부 보안관련 라이브러리를 사용한다.
    3. 보안 솔루션을 사용한다.

 

 

Persistence Framework

📌 애플리케이션에서 데이터를 영구적으로 저장하고 관리하기 위해 데이터베이스와 같은 저장소와의 상호 작용을 단순화하는 소프트웨어 도구이다.

 

Persistence Framework 개요 

  • JDBC의 한계
    1. 간단한 SQL을 실행하는 경우에도 중복된 코드가 너무 많았다.
public class PreparedStatementExample {
    public static void main(String[] args) {
        try {
						// ojdbc6.jar[oracle.jdbc.driver.OracleDriver] 파일을 라이브러리에 추가한다.
						Class.forName("oracle.jdbc.driver.OracleDriver");
						
						// 1. Connection
            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/mydatabase", "username", "password");
            String query = "SELECT * FROM employees WHERE department = ?";
            
            // 2. Statement
            PreparedStatement preparedStatement = connection.prepareStatement(query);
            
            // 값을 설정
            preparedStatement.setString(1, "HR");
            
            // 3. ResultSet
            ResultSet resultSet = preparedStatement.executeQuery();
            
            while (resultSet.next()) {
                // 결과 처리 코드
            }
            
            resultSet.close();
            preparedStatement.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  • onnection, Prepared Statement, ResultSet 등
  1. DB에 따라 일관성 없는 정보를 가진 채로 Checked Exception(SQL Exception) 처리를 한다.
    • Checked Exception인 SQLException은 개발자가 명시적으로 처리해야 한다.
    • 각 DBMS는 고유한 SQL 문법과 오류 코드 체계를 가지고 있다.
      • JDBC에서 발생하는 SQLException은 이러한 DBMS에 따라 달라질 수 있으며, 예외 메시지나 코드도 DB마다 다를 수 있다. 즉, 모두 그에 맞게 처리해야 한다.
  2. Connection과 같은 공유 자원을 제대로 반환하지 않으면 한정된 시스템 자원(CPU, Memory)에 의해 서버가 다운되는 등의 문제가 발생한다.
  3. SQL Query를 개발자가 직접 작성한다.
    • 중복 적인 쿼리 및 코드 작성이 필요하다.
    • 대부분의 테이블에 CRUD하는 쿼리가 포함된다.
  • Persistence Framework의 등장
    • JDBC 처럼 복잡함이나 번거로움 없이 간단한 작업만으로 Database와 연동되는 시스템
    • 모든 Persistence Framework는 내부적으로 JDBC API를 이용한다.
    • preparedStatement를 기본적으로 사용한다.
    → 위에서 JDBC를 설명한 이유.
    • 크게 SQL Mapper, ORM 두가지로 나눌 수 있다.
  • JDBC, SQL MAPPER, ORM의 공통점
    • 영속성(Persistence) 데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 특성, 영구히 저장되는 특성

 

 

Persistence Framework의 역할

  1. 데이터 접근 로직 간소화:
    • JDBC와 같은 저수준 API의 반복 작업을 제거.
  2. 객체와 관계형 데이터 매핑:
    • 객체지향 언어(Java)의 데이터와 관계형 데이터베이스 간의 불일치를 해결.
  3. 트랜잭션 관리:
    • 데이터 작업을 트랜잭션 단위로 처리하여 데이터 무결성 보장.
  4. 데이터베이스 독립성:
    • 특정 데이터베이스에 종속되지 않고, 다양한 RDBMS와 연동 가능.
  5. 쿼리 생성 최적화:
    • 동적 쿼리 생성을 자동화하거나 간소화.

 

 

Persistence Framework의 특징

  • ORM (Object-Relational Mapping):
    • 객체 모델과 관계형 데이터베이스를 매핑.
    • 데이터베이스 작업을 객체지향 방식으로 처리.
  • SQL 추상화:
    • SQL 쿼리를 자동 생성하거나 간소화된 API로 대체.
  • 트랜잭션 지원:
    • ACID 특성을 보장하는 트랜잭션 처리.
  • 데이터베이스 연결 관리:
    • 커넥션 풀을 사용해 데이터베이스 연결 성능을 최적화.

 

주요 Persistence Framework

a. Hibernate

  • 설명: JPA(Java Persistence API)의 구현체 중 하나로 가장 널리 사용됨.
  • 특징:
    • 완전한 ORM 지원.
    • 동적 쿼리 및 캐시 지원.
    • 다양한 데이터베이스와 호환 가능.

b. JPA (Java Persistence API)

  • 설명: Java 표준 ORM API로, 데이터베이스와 객체 간 매핑을 정의.
  • 특징:
    • Hibernate, EclipseLink와 같은 구현체가 필요.
    • 표준화된 인터페이스 제공.

c. MyBatis

  • 설명: SQL 매핑 기반의 Persistence Framework.
  • 특징:
    • SQL을 직접 작성하여 세부적인 제어 가능.
    • 자동 매핑 기능 지원 (SQL 결과와 객체 매핑).

d. Spring Data JPA

  • 설명: JPA를 더 간단하게 사용할 수 있도록 지원하는 Spring 모듈.
  • 특징:
    • 기본 CRUD 작업을 간소화.
    • Repository 패턴 지원.

e. EclipseLink

  • 설명: JPA의 또 다른 구현체로, Oracle과 밀접한 통합.
  • 특징:
    • 성능 최적화와 고급 기능 지원.

 

ORM과 SQL 매핑 프레임워크 비교

  ORM (Hibernate, JPA) SQL 매핑 (MyBatis)
쿼리 작성 방식 자동 생성 (HQL, JPQL) 직접 작성 (SQL 문장 사용).
생산성 높은 추상화로 간단한 작업은 편리. 복잡한 쿼리 작성에 유리.
유연성 쿼리 최적화가 어려울 수 있음. 세부적인 제어 가능.
학습 곡선 ORM의 개념 이해 필요. SQL 지식만으로 쉽게 접근 가능.
사용 사례 CRUD 중심의 애플리케이션. 복잡한 데이터 조작 및 최적화된 쿼리 필요.

 

 

Persistence Framework의 장점

  1. 생산성 향상:
    • 데이터 접근 로직을 간소화.
  2. 데이터베이스 독립성:
    • 다양한 RDBMS와 호환 가능.
  3. 코드 간결화:
    • CRUD 작업의 반복 코드를 제거.
  4. 트랜잭션 관리:
    • ACID 특성을 쉽게 적용 가능.
  5. 객체-관계 매핑:
    • 객체지향적인 데이터 접근 가능.

 

Persistence Framework의 단점

  1. 복잡성 증가:
    • ORM 개념 학습과 설정이 필요.
  2. 쿼리 최적화 한계:
    • ORM에서는 복잡한 SQL 튜닝이 어려울 수 있음.
  3. 퍼포먼스 문제:
    • 자동 생성된 쿼리가 비효율적일 수 있음.
  4. 추상화의 비용:
    • 프레임워크의 내부 동작을 이해해야 문제를 해결 가능.

 

Persistence Framework 선택 가이드

요구사항 권장 프레임워크
간단한 CRUD 애플리케이션 Spring Data JPA, Hibernate
복잡한 SQL 쿼리 작업 MyBatis
높은 데이터베이스 독립성 필요 JPA
빠르고 세부적인 SQL 제어 필요 MyBatis
대규모 트랜잭션 관리 및 확장성 요구 Hibernate, JPA

 

 

 

SQL Mapper

📌 SQL 쿼리와 객체(Object) 간의 매핑을 지원하는 도구입니다. 직접 작성한 SQL 문의 실행 결과와 객체(Object)의 필드를 Mapping하여 데이터를 객체화한다. 대표적인 SQL Mapper로 Spring JDBC Template, MyBatis가 있다.

 

  • Spring JDBC Template
    • Spring Framework에서 제공하는 JDBC 작업을 단순화하고 개선한 유틸리티 클래스
  • JDBC Template의 장점
    1. 간편한 데이터베이스 연결
      1. 손수 적었던 Connection 관련 코드들을 yml 혹은 properties 파일에 설정만으로 해결한다.
    2. Prepared Statement를 사용한다.
    3. 예외 처리와 리소스 관리
      1. DB Connection을 자동으로 처리하여 리소스 누수를 방지한다.
    4. 결과 집합(ResultSet) 처리
      1. 데이터를 자바 객체로 변환할 수 있도록 돕는다.
    5. 배치 처리 작업을 지원한다.
      1. 매일 동일한 시간에 수행되는 쿼리, 주로 통계에 사용된다.
// 1. XML OR Gradle에 Spring JDBC 의존성 추가
// 2. application.properties OR application.yml에 데이터베이스 연결 설정

@RestController
public class MemberController {
    private final MemberRepository memberRepository;

    public MemberController(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @GetMapping("/members")
    public List<Member> findById(Long id) {
        return memberRepository.findById(id);
    }
}

// Member Object
public class Member {
    private Long id;
    private String name;
    private int age;

    // Getter and Setter methods
}

// Repository Anotation의 역할에 대해 공부해주세요.
@Repository
public class MemberRepository {
    private final JdbcTemplate jdbcTemplate;

    public MemberRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

		// Member 객체로 리턴한다.
    public List<Member> findById(Long id) {
        String query = "SELECT * FROM MEMBER WHERE id = " + id;
        return jdbcTemplate.query(query, (rs, rowNum) -> {
            Member member = new Member ();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            member.setAge(rs.getInt("age"));
            return member;
        });
    }
}

 

 

대표적인 SQL Mapper

  1. MyBatis:
    • SQL 기반의 가장 널리 사용되는 SQL Mapper.
    • 간단한 XML 매핑 설정으로 SQL과 객체를 연결.
  2. JDBC Template (Spring):
    • Spring에서 제공하는 SQL 처리 도구.
    • SQL Mapper와 비슷한 역할 수행.
  3. iBatis:
    • MyBatis의 이전 버전으로, MyBatis로 발전됨.

 

SQL Mapper의 한계

  1. SQL을 직접 다룬다.
  2. 특정 DB에 종속적으로 사용하기 쉽다.
    1. DB마다 Query문, 함수가 조금씩 다르다.
    → 다른 DB를 사용하면 쿼리도 변경해야할 가능성이 높다.
  3. 테이블마다 비슷한 CRUD SQL, DAO(Data Access Object) 개발이 반복된다. → 코드 중복
  4. 테이블 필드가 변경될 시 이와 관련된 모든 DAO의 SQL문, 객체의 필드 등을 수정해야 한다.
    1. 코드상으로 SQL과 JDBC API를 분리했지만 논리적으로 강한 의존성을 가지고 있다.
  5. 객체와의 관계는 사라지고 DB에 대한 처리에 집중하게 된다. → SQL 의존적인 개발
  • 관계형 DB와 객체지향의 패러다임 불일치
    • 객체지향으로 설계된것을 관계형 DB에 저장하기란 어렵다.
    • 테이블에 저장한 데이터를 다시 객체화 하는것도 어렵다.
// 1. 객체 안의 객체
public class Member {
	// 필드들..
	private Team team;
} -> ERD?

// 2. 상속 구조
public class Member extends Person {
	// 필드들..
} -> ERD?

// 3. extends, implements
public class Member extends Person implements Workable {
  // 필드들..
} -> ERD?

 

  • 객체지향 : 캡슐화, 추상화, 상속, 다형성 → 객체 중심
  • 관계형 데이터베이스(RDB) → 데이터 중심의 구조
  • 각각 지향하는 목적이 다르기 때문에 사용 방법과 표현 방식에 차이가 있다.

 

요약

  1. JDBC → SQL Mapper(JDBC Template, Mybatis) → ORM(JPA Hibernate)
  2. Persistence Framework는 모두 JDBC API를 내부적으로 사용하고 있다.
  3. SQL Mapper, ORM 들은 내부적으로 Prepared Statement를 사용하고있다.
  4. SQL Mapper, ORM 들은 Database Connection등 리소스 관리를 자동으로 해주고있다.
  5. Persistence Framework는 저장소와의 상호 작용을 단순화 한다.

 

 

MyBatis

📌 SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL을 분리하여 관리되도록 만들어준다.

  • SQL과 Java Code의 분리가 핵심
  • Query를 JAVA에서 XML로
    • 복잡한 JDBC 코드가 사라진다.
    • ResultSet과 같이 결과값을 맵핑하는 객체가 없다.
    • 설정이 간단하다.
    • 관심사를 분리한다. → SQL 을 따로 관리한다.
    • XML 안에있는 SQL을 Java의 메소드에 매핑해준다.
  • MyBatis 장점
    1. 자동으로 Connection 관리를 해주면서 JDBC 사용할 때의 중복 작업 대부분을 없애준다.
    2. DB 결과 집합을 자바 객체로 매핑할 수 있다.
    3. 복잡한 쿼리나 다이나믹하게(동적쿼리) 변경되는 쿼리 작성이 쉽다.
      1. 상황에 따라 분기처리(IF)를 통해 쿼리를 동적으로 만들어주는것.
    4. 관심사 분리 - DAO로부터 SQL문을 분리하여 코드의 간결성 및 유지보수성이 향상된다.
    5. 쿼리 결과를 캐싱하여 성능을 향상시킬 수 있다.
// User 클래스
public class User {
    private Long id;
    private String userName;
    private String email;

    // Getter and Setter methods
}



// Mapper Interface 생성 : SQL 쿼리와 매핑을 정의하는 인터페이스
public interface UserMapper {
    User getUserById(Long id);
}


public class Main {
    public static void main(String[] args) {
        String resource = "mybatis-config.xml";
        try (Reader reader = Resources.getResourceAsReader(resource)) {
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            try (SqlSession session = sqlSessionFactory.openSession()) {
                UserMapper userMapper = session.getMapper(UserMapper.class);
                User user = userMapper.getUserById(1L);
                System.out.println(user.getId() + ", " + user.getUsername() + ", " + user.getEmail);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

Mapper XML

SQL 쿼리와 객체 매핑을 정의한다.

<mapper namespace="com.example.UserMapper">
    <select id="getUserById" resultType="com.example.User">
        SELECT id, userName, email
        FROM users
        WHERE id = #{id}
    </select>
</mapper>

MyBatis 설정 파일 작성

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/mydb" />
                <property name="userName" value="userName" />
                <property name="password" value="password" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/example/UserMapper.xml" />
    </mappers>
</configuration>

 

+ Recent posts