Singleton 패턴 + 의존성 주입 (DI) + Cookie, Session, Token + Servlet Filter +@ Entity + 패러다임 불일치 + 영속성 컨텍스트

ssongyi·2025년 4월 4일
0

Java/Spring TIL

목록 보기
7/11

Singleton 패턴의 원리

: 클래스의 인스턴스를 오직 하나만 생성하도록 보장하는 디자인 패턴
즉, "같은 객체를 계속 새로 만들지 말고, 한 번 만든 것을 재사용하자!" 라는 뜻

  • 인스턴스는 단 한 번만 생성됨
  • 전역적으로 공유됨
  • 메모리 효율이 좋고, 객체 생성 비용 절감
public class Singleton {

    // static으로 미리 한 개 생성
    private static final Singleton instance = new Singleton();

    // 생성자 private → 외부에서 new로 생성 못함
    private Singleton() {}

    // 인스턴스 반환하는 유일한 메서드
    public static Singleton getInstance() {
        return instance;
    }
}
Singleton obj1 = Singleton.getInstance();
Singleton obj2 = Singleton.getInstance();

System.out.println(obj1 == obj2);  // true (같은 인스턴스!)

Spring 에서 Singleton 은 어떻게 적용될까?

Spring 은 기본적으로 모든 Bean 을 Singleton Scope 으로 관리

@Service
public class MemberService {
    // 이 클래스는 싱글톤으로 관리됨
}

싱글톤 패턴과 Spring 의 차이점


Spring Container ?

Spring 에서 객체를 관리하는 핵심 엔진(컨테이너)
: 개발자가 직접 객체를 생성하고 관리하는 대신,
Spring 이 대신 객체를 생성하고 생명주기를 관리해줌

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

MemberService memberService = context.getBean(MemberService.class);

여기서 context --> Spring Container

컨테이너의 주요 역할


Spring Bean ?

Spring Container 가 생성하고 관리하는 객체

즉, Bean = Spring 에 의해 관리되는 객체이고,
Spring Container 는 그 객체(Bean) 를 만들고, 넣고, 꺼내주고, lifecycle 을 관리

Bean 등록 방법

  1. 어노테이션 기반 등록
@Service // or @Component, @Repository, @Controller
public class MemberService {
}
  1. Java Config 수동 등록
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService();
    }
}

Spring Bean 사용 방법

  1. 자동 주입 방법 - 생성자 주입 (권장)
    : 불변성 + 테스트 편의성
@Service
public class OrderService {

    private final MemberService memberService;

    public OrderService(MemberService memberService) {
        this.memberService = memberService; // 자동 주입!
    }
}
  1. 필드 주입
@Autowired
private MemberService memberService;

의존성 주입 (DI) ?

Dependency Injection, DI
: 객체 간의 관계를 외부에서 주입해주는 방식
: Spring 에서는 객체 간의 결합도를 낮추고, 유연하게 설계하기 위해 DI 를 적극 활용함

의존성 주입의 종류

생성자 주입(Constructor Injection) - 가장 권장

@Service
public class OrderService {

    private final MemberService memberService;

    public OrderService(MemberService memberService) {
        this.memberService = memberService;
    }
}
  • @Autowired 생략 가능 (Spring 4.3 이상)
  • 테스트하기 좋고, final 키워드로 불변성 유지 가능
  • 의존성 주입이 반드시 필요한 경우에 적합

의존성 주입을 위한 어노테이션


Cookie, Session, Token ?

클라이언트(브라우저) 에 저장되는 작은 데이터 조각

  • Set-cookie 응답 헤더로 저장
  • 이후 요청마다 자동으로 서버에 쿠키를 담아 전송
  • 로그인 토큰이나 사용자 ID 등 저장 가능

📌 주의사항

  • 클라이언트에 저장되므로 보안에 민감한 정보는 금지
  • XSS/CSRF 공격에 노출되기 쉬움
    - HttpOnly, Secure, SameSite 속성 활용 필요

Session

서버가 클라이언트를 식별하기 위해 세션 ID 를 발급하고 서버에 상태를 저장

  • 클라이언트는 세션 ID 만 쿠키로 가지고 있음 (JSESSIONID)
  • 실제 인증/사용자 정보는 서버에 저장됨

📌 특징

  • 서버 메모리 사용 --> 많은 사용자 접속 시 부하 증가
  • 브라우저 닫거나 로그아웃 시 세션 종료
HttpSession session = request.getSession();
session.setAttribute("sessionKey", memberId);

Spring에서는 HttpSession으로 쉽게 사용 가능

Token (JWT - JSON Web Token)

인증 정보를 자체적으로 포함하는 JSON 기반의 토큰

  • 상태 저장 없이, 토큰 자체로 인증 가능 (Stateless)
  • 클라이언트가 Authorization 헤더에 담아서 요청
  • 일반적으로 Access Token + Refresh Token 조합 사용

📌 장점

  • 서버 확장성 뛰어남
  • 모바일, SPA, 외부 API 호출 등에 적합
  • 서명(Signature) 기반 위조 방지 가능

📌 단점

  • 토큰 자체탈취 시 위험
  • 토큰 만료 전까지는 강제 로그아웃 어려움

로그인 상태 관리 방식

Spring 적용 팁


JWT 를 활용한 인증/인가

[1] 로그인 요청 (ID/PW) →  
[2] 서버에서 사용자 인증 후 JWT 생성 →  
[3] JWT를 클라이언트에 응답 →  
[4] 클라이언트는 JWT를 요청 헤더(Authorization)에 포함시켜 요청 →  
[5] 서버는 JWT를 검증 후 사용자 인가 처리

JWT 구성 (3가지 파트)


Servlet Filter ?

클라이언트의 HTTP 요청서블릿(Controller) 사이에서 동작
요청/응답을 가로채어 사전/사후 처리하는 컴포넌트

즉, Spring Controller 진입 전에 동작하고,
필요하면 응답(Response) 도 조작 가능

: 특히 로그인, 인증, 로깅 등에 자주 사용됨

[클라이언트] → [Filter] → [DispatcherServlet] → [Controller]
                                 ↑
                         (응답 시 다시 거침)

필터 주요 활용 예시

필터 vs 인터셉터 vs AOP

필터 --> 인터셉터 --> AOP 순으로 더 세분화되고 구체적인 처리 가능

📌 주의사항

  • chain.doFilter() 꼭 호출해야 요청이 다음 단계로 넘어감 !
  • HttpServletRequest, HttpServletResponse로 다운캐스팅해야 header, session 등 다룰 수 있어요.
  • 로그인 인증 필터 작성 시, 화이트리스트(로그인/회원가입) 경로는 예외 처리 필요!

@Entity ?

JPA 가 관리할 수 있는 객체임을 선언하는 어노테이션

즉, 해당클래스는 DB 테이블과 매핑되며,
Spring Data JPA 가 이 클래스의 인스턴스를 통해
데이터베이스 CRUD 작업을 자동으로 처리할 수 있게 됨

왜 엔티티를 쓰는가 ?

Entity 를 정의할 때 주의사항


패러다임 불일치 문제 ?

: JPA 가 등장하게 된 배경 !
: 왜 객체지향 프로그래밍에서 JPA 쓰는지 알 수 있음

패러다임 불일치(Paradigm Mismatch) ?

"객체(Object)" vs "관계형 데이터베이스(Relational DB)" 간의 생각 방식 차이

즉, 객체는 계층적이고 참조 기반,
RDB는 평면적이고 키 기반이기 때문에 이 둘을 바로 연결해서 쓰기 어려움

패러다임 불일치 상황

class Member {
    private String name;
    private Team team; // 객체 간 참조
}

이걸 MySQL 에 저장하려면

CREATE TABLE member (
  id BIGINT AUTO_INCREMENT,
  name VARCHAR(255),
  team_id BIGINT, -- 외래키
  PRIMARY KEY (id)
);
  • 객체에서는 team 필드가 객체를 참조하는데,
  • DB 에서는 team_id 가 외래 키 값으로만 표현됨
    👆 이런 차이가 "패러다임 불일치"

JPA 등장 목적

JPA 는 객체와 관계형 데이터베이스 사이의 불일치를 해결하기 위한 ORM 기술

JPA 는 아래를 자동으로 처리해 줌

  • 객체 <-> 테이블 매핑
  • 객체 필드 <-> 컬럼 자동 바인딩
  • 객체 그래프 탐색 <-> 조인 쿼리 생성
  • 변경 감지 --> update 자동 반영
  • 연관관계 매핑 --> 외래키 매핑 자동화

영속성 컨텍스트 ?

엔티티를 저장(persist) 하면, JPA 가 엔티티를 관리하는 메모리 공간
쉽게 말해,
JPA = DB 와의 중간에서 엔티티를 관리하는 1차 캐시
--> 이건 엔티티 객체를 메모리에 저장해서 성능을 높이고, 일관성을 보장하는 장치

영속성 컨텍스트 흐름 요약

  1. EntityManager.persist(member)
    → member 객체는 DB에 바로 저장 ❌
    영속성 컨텍스트(1차 캐시)에 저장

  2. 트랜잭션이 끝나고 커밋 시
    → JPA가 쿼리를 생성해서 DB에 반영

주요 특징

1차 캐시

Member m1 = em.find(Member.class, 1L); // DB에서 가져옴 → 1차 캐시에 저장
Member m2 = em.find(Member.class, 1L); // 1차 캐시에서 가져옴 (쿼리 ❌)
  • 같은 트랜잭션 안에서 동일 ID로 두 번 조회하면 쿼리는 딱 한 번만 나감
  • JPA가 Map<PK, Entity> 형태로 1차 캐시에 저장해둠
    - SELECT 가 많이 나가는 걸 막을 수 있음(성능 향상)

동일성 보장

Member m1 = em.find(Member.class, 1L);
Member m2 = em.find(Member.class, 1L);
System.out.println(m1 == m2); // true
  • 두 객체는 완전히 동일한 인스턴스(주소) 를 반환함
  • JPA 는 같은 ID 로 2개 이상 만들지 않도록 1차 캐시에서 먼저 조회함
    - 이 특징 덕분에 객체를 비교할 때 == 가능

쓰기 지연 (Write-Behind)

em.persist(member1); // INSERT ❌
em.persist(member2); // INSERT ❌
// DB에 아직 전송되지 않음

em.getTransaction().commit(); 
// 그때서야 INSERT member1, INSERT member2 실행
  • persist() 할 때 DB 에 바로 INSERT 쿼리를 날리지 않고
  • 트랜잭션 커밋 시점에 한꺼번에 쿼리를 보냄
    - 쿼리 수를 줄이고 성능을 높이기 위해 사용됨

변경 감지 (Dirty Checking)

Member member = em.find(Member.class, 1L);
member.setName("변경된 이름"); // setter 호출만 했을 뿐인데?

em.getTransaction().commit(); // update 쿼리 자동 생성됨
  • JPA 는 최초 상태를 복사해 두고, 커밋 시점에 현재 상태와 비교함
  • 바뀐 필드가 있다면, 자동으로 UPDATE 쿼리 생성
    - 개발자가 em.update() 같은 걸 호출할 필요 X

JPA 가 트랜잭션 안에서 객체 상태를 감지하고, SQL 을 생성!

flush() 는 트랜잭션 커밋 전 DB 에 쿼리를 보내는 동작이고, 커밋 시 자동으로 실행됨

0개의 댓글