02.16
1. 스프링은 IoC 컨테이너를 가진다
**IoC(Inversion of Control)**는 **'제어의 역전'**이란 뜻이다. 기존 자바 프로그래밍에서는 개발자가 직접 객체를 생성(new)하고, 메소드를 호출하며 프로그램의 흐름을 제어했다. 하지만 스프링에서는 이 제어권이 개발자가 아닌 **프레임워크(스프링 컨테이너)**로 넘어간다.
- 개발자: 객체의 설계도(클래스)만 작성하고, 설정(Configuration)만 해준다.
- 스프링 컨테이너: 알아서 객체(Bean)를 생성하고, 관리하고, 필요할 때 없애는 **생명주기(Lifecycle)**를 전담한다.
- 즉, **"내가 호출하는 게 아니라, 프레임워크가 내 코드를 호출한다"**는 것이다.
2. 스프링은 DI를 지원한다
**DI(Dependency Injection)**는 **'의존성 주입'**이다. IoC가 큰 개념이라면, DI는 그것을 실현하는 구체적인 방법이다. IoC 컨테이너에 등록된 객체(Bean)들끼리 관계(의존성)를 맺어주는 것을 말한다.
- 기존 방식: A 객체가 B 객체를 사용할 때, A가 직접 B를 생성했다. (A a = new B();) -> 결합도가 높음.
- DI 방식: A는 B가 필요하다고 선언만 하고, **외부(스프링 컨테이너)에서 생성된 B를 A에게 주입(Injection)**해준다. -> 결합도가 낮아짐(Loose Coupling).
결국 DI 덕분에 부품을 갈아 끼우듯이 코드를 유연하게 변경할 수 있다. 실행 방법은 간단히 다음과 같다.
@Service
public class StockService {
private final StockRepository stockRepository;
// 생성자 주입 (DI)
@Autowired // (생성자가 하나일 땐 생략 가능)
public StockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
}
02.17
1. 필터(Filter)와 인터셉터(Interceptor)
서버는 기본적으로 인증(Authentication)과 인가(Authorization) 처리를 해야 하는데, 스프링 웹 애플리케이션에서는 이 역할을 필터와 인터셉터가 나누어 담당한다.
1) 필터 (Filter)
- 위치: Web Container (Tomcat) 내부에 존재한다. (스프링 컨테이너 밖)
- 역할: 요청이 스프링(DispatcherServlet)에 도달하기 전에 실행된다.
- 용도: 인코딩 변환, 보안(XSS 방어), Spring Security 등이 여기서 동작한다. (web.xml 설정을 따름)
2) 인터셉터 (Interceptor)
- 위치: Spring Container 내부에 존재한다.
- 역할: 스프링의 DispatcherServlet이 컨트롤러를 호출하기 전/후에 가로채서 실행된다.
- 용도: 로그인 세션 체크, 권한 확인, 로그 남기기 등 비즈니스 로직과 밀접한 처리를 담당한다.
1-1. Spring Security
스프링 시큐리티는 필터(Filter) 기반으로 동작하는 강력한 보안 프레임워크다. 톰캣(서블릿 컨테이너)의 필터 체인에 **DelegatingFilterProxy**라는 특수한 필터를 끼워 넣어서 동작한다. 즉, 톰캣에서 요청을 먼저 가로채서 보안 검사를 수행한 뒤, 안전한 요청만 스프링 컨테이너로 들여보낸다.
DelegatingFilterProxy (위임 필터 프록시)
- 문제점: 톰캣의 '필터'는 스프링 컨테이너(Bean)의 존재를 모른다. 그래서 필터에서는 @Autowired로 스프링 빈(Service 등)을 주입받아 사용할 수 없다. 보안 검사를 하려면 DB 조회도 하고 그래야 하는데 말이다.
- 해결책 (다리 놓기):
- 스프링은 톰캣 필터 자리에 **DelegatingFilterProxy**라는 녀석을 대리인(Proxy)으로 세워둔다.
- 요청이 들어오면 이 대리인이 요청을 가로챈다.
- 그리고 스프링 컨테이너 내부에 있는 진짜 보안 담당 빈(FilterChainProxy)에게 "야, 이거 네가 처리해"라고 **위임(Delegate)**한다.
- 덕분에 톰캣 필터 단계에서도 스프링의 강력한 기능(Bean 사용)을 끌어다 쓸 수 있게 된다.
2. 어노테이션(Annotation)과 리플렉션(Reflection)
1) 어노테이션 (@) 코드에 붙이는 '주석' 또는 **'마킹(Marking)'**이다. 어노테이션 자체는 아무런 기능이 없다. "이 클래스는 객체로 만들어줘", "이 변수는 연결해줘"라고 표시만 해두는 것이다.
- @Component: "이 클래스를 스캔해서 빈(Bean)으로 등록해 주세요." (메모리 로딩 요청)
- @Autowired: "여기에 맞는 빈을 찾아서 의존성을 주입해 주세요." (연결 요청)
2) 리플렉션 (Reflection) 실제로 일을 하는 **'일꾼'**이다. 프로그램이 실행될 때(Runtime), 리플렉션 API가 클래스들에 붙어있는 어노테이션을 싹 스캔(Scan)한다.
- 스캔하다가 @Component가 붙은 클래스를 발견하면 → new로 객체를 생성해서 메모리(Heap)에 올린다.
- 스캔하다가 @Autowired가 붙은 필드를 발견하면 → 메모리에 있는 적절한 객체를 찾아서 넣어준다.
즉, **어노테이션은 '지시서'이고, 리플렉션은 그 지시를 보고 실행하는 '실행 엔진'**이다.
3. MessageConverter
클라이언트(웹/앱)와 서버(자바)가 통신할 때, 서로 다른 언어를 사용해도 데이터를 주고받을 수 있는 이유는 MessageConverter 덕분이다.
- 자바 객체(Object)는 자바만 이해할 수 있다.
- 그래서 통신할 때는 전 세계 공용어인 JSON으로 바꿔서 보낸다.
- 동작: 컨트롤러에서 데이터를 반환할 때, 스프링의 HttpMessageConverter(기본 구현체: Jackson 라이브러리)가 자동으로 개입한다.
- 갈 때: 자바 객체 → JSON 변환
- 올 때: JSON → 자바 객체 변환
1. JPA (Java Persistence API)와 ORM
JPA는 자바 진영의 ORM(Object-Relational Mapping) 기술 표준(인터페이스)입니다.
1) ORM이란?
- "객체(Object)는 객체대로 설계하고, 관계형 데이터베이스(Relational DB)는 관계형 데이터베이스대로 설계해라. 내가 중간에서 매핑(Mapping)해 줄게!"라는 기술입니다.
- 과거에는 DB 테이블을 먼저 뼈대로 잡고 거기에 맞춰서 자바 클래스를 만들었지만, ORM을 사용하면 자바 객체(Entity)를 먼저 설계합니다.
- 설정(ddl-auto)에 따라 자바 객체를 분석해서 자동으로 DB 테이블을 생성해주기도 합니다.
2) JPA의 장점
- 개발자가 쿼리문(SQL)을 직접 한 땀 한 땀 작성하고 DB 연결 객체를 세팅하는 단순 반복 작업(Boilerplate Code)을 획기적으로 줄여줍니다.
- 마치 자바 컬렉션(List, Map)에 데이터를 넣고 빼듯이 DB를 다룰 수 있게 해줍니다.
2. JPA의 심장, 영속성 컨텍스트 (Persistence Context)
영속성 컨텍스트는 **"엔티티(객체)를 영구 저장하는 환경"**이라는 뜻으로, 애플리케이션과 DB 사이에서 객체를 보관하는 가상의 공간입니다.
JPA가 성능이 좋고 마법처럼 동작하는 이유는 이 영속성 컨텍스트가 제공하는 다음과 같은 기능들 때문입니다.
1) 1차 캐시 (First-level Cache)
- DB에서 데이터를 조회하면 영속성 컨텍스트 내부에 있는 '1차 캐시'에 객체 형태로 저장해 둡니다.
- 이후 똑같은 데이터를 찾을 때, DB까지 가지 않고 1차 캐시에서 바로 꺼내오므로 성능이 매우 빠릅니다.
2) 쓰기 지연 (Transactional Write-behind)
- 데이터를 저장할 때마다 매번 DB에 INSERT 쿼리를 날리지 않습니다.
- 영속성 컨텍스트 내부의 '쓰기 지연 SQL 저장소'에 쿼리를 차곡차곡 모아두었다가, 트랜잭션이 **커밋(Commit)**되는 순간 DB로 한 번에 쫙 보냅니다. (네트워크 부하 감소)
3) 변경 감지 (Dirty Checking) ⭐핵심
- 자바 코드에서 객체의 데이터(필드 값)를 수정하기만 해도, JPA가 알아서 DB에 UPDATE 쿼리를 날려줍니다.
- 원리: 영속성 컨텍스트는 데이터가 처음 들어올 때의 최초 상태(스냅샷)를 기억하고 있습니다. 커밋 시점에 스냅샷과 현재 객체 상태를 비교해서, 변경된 부분이 있으면 자동으로 UPDATE 쿼리를 생성해 DB에 반영합니다.
3. 패러다임 불일치 (Impedance Mismatch) 해결
객체지향 프로그래밍(OOP)과 관계형 데이터베이스(RDB)는 태생부터 사상이 다릅니다. 이를 패러다임 불일치라고 합니다.
- DB의 방식: 외래 키(Foreign Key)를 사용해서 다른 테이블과 연관 관계를 맺고 JOIN을 합니다. 테이블 안에는 다른 테이블을 통째로 넣을 수 없습니다.
- 객체의 방식: 객체는 참조(Reference)를 사용해서 다른 객체와 연관 관계를 맺습니다. (예: member.getTeam())
JPA의 해결: JPA 없이 개발하면 개발자가 중간에서 SQL을 통해 외래 키를 참조로, 참조를 외래 키로 일일이 변환하는 노가다를 해야 합니다. 하지만 JPA를 사용하면 자바에서는 완벽하게 객체지향적으로 Member 객체 안에 Team 객체를 넣어서 설계해도, JPA가 알아서 외래 키로 변환해 INSERT 해주고, 조회할 때는 알아서 JOIN 쿼리를 날려 객체로 조립해 줍니다.
4. 엔티티의 생명 주기 (Entity Lifecycle)
영속성 컨텍스트를 이해했다면, 이제 객체(엔티티)가 이 컨텍스트 안에서 어떻게 살아가는지 4가지 상태를 알아야 합니다.
1) 비영속 (New / Transient)
- 상태: 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태.
- 설명: 그냥 자바에서 new 키워드로 객체를 막 생성한 상태입니다. JPA는 이 객체의 존재를 모릅니다.
- 코드: Member member = new Member();
2) 영속 (Managed)
- 상태: 영속성 컨텍스트에 관리되는 상태.
- 설명: 객체를 영속성 컨텍스트에 집어넣은 상태입니다. 이제부터 JPA가 이 객체의 변경 사항을 추적(Dirty Checking)하기 시작합니다.
- 코드: em.persist(member); (또는 DB에서 em.find()로 막 조회해 온 상태)
3) 준영속 (Detached)
- 상태: 영속성 컨텍스트에 저장되었다가 분리된 상태.
- 설명: 더 이상 JPA가 관리하지 않습니다. 객체의 값을 바꿔도 DB에 반영(UPDATE)되지 않습니다.
- 코드: em.detach(member); (특정 객체만 분리) 또는 em.clear(); (컨텍스트 전체 초기화)
4) 삭제 (Removed)
- 상태: 객체를 삭제한 상태.
- 설명: 영속성 컨텍스트와 실제 DB에서 모두 날려버리겠다고 표시한 상태입니다. 커밋 시점에 DELETE 쿼리가 날아갑니다.
- 코드: em.remove(member);
5. 연관관계 매핑 (Association Mapping) ⭐핵심⭐
객체 지향과 관계형 DB의 가장 큰 차이점(패러다임 불일치)을 해결하는 JPA의 진가입니다.
- DB: '외래 키(FK)' 하나로 양쪽 테이블을 조인(JOIN)해서 넘나듭니다. (방향이 없음)
- 객체: '참조(Reference)'를 통해서만 이동할 수 있습니다. (A -> B로 갈 순 있지만, B -> A로 가려면 B 안에도 A의 참조가 있어야 함)
이 차이를 극복하기 위해 나온 개념이 단방향/양방향 매핑과 연관관계의 주인입니다.
① 단방향 vs 양방향
- 단방향 매핑: 회원(Member) 객체 안에 팀(Team) 객체를 참조하는 필드가 있어서, 회원에서 팀으로는 갈 수 있지만 팀에서는 회원을 모르는 상태입니다. (@ManyToOne 사용)
- 양방향 매핑: 팀(Team) 객체 안에도 회원들을 담는 리스트(List<Member>)를 만들어서 서로 참조하게 만든 상태입니다.
② 연관관계의 주인 (Owner of the Relationship)
양방향 매핑을 할 때 개발자들을 가장 괴롭히는 개념입니다. DB 입장에서는 '회원'과 '팀' 테이블 중 외래 키(FK) 하나만 관리하면 되는데, 객체는 양쪽에서 서로를 참조하고 있습니다. "그럼 둘 중 누구의 값이 바뀔 때 DB의 외래 키를 업데이트해야 해?" 라는 딜레마가 생깁니다. 이때 룰을 정하는 것이 **'연관관계의 주인'**입니다.
- 규칙 1: 외래 키(FK)가 있는 곳이 무조건 주인이다.
- '다(N)' 쪽인 회원(Member) 테이블에 팀(Team)의 외래 키가 있으므로, Member 객체가 연관관계의 주인이 됩니다.
- 규칙 2: 주인이 아닌 쪽은 읽기만 가능하다.
- 주인이 아닌 Team 객체는 외래 키를 변경할 권한이 없습니다. (DB에 영향을 주지 못함)
- 주인이 아닌 쪽에 mappedBy 속성을 적어줍니다. ("나는 진짜 주인이 아니라, 저쪽 필드에 의해 매핑된 거울일 뿐이야"라는 뜻)
// 연관관계의 주인 (다, N)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
// 외래 키를 관리하는 진짜 주인!
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
// 주인이 아님 (일, 1)
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
// 나는 주인이 아니다! Member 클래스의 'team' 필드의 거울일 뿐이다.
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
💡 실무 한 스푼 (Backend Insight)
1. 실무에서는 양방향 매핑을 웬만하면 피하라! 처음부터 무턱대고 양방향 매핑을 걸면 코드가 복잡해지고 무한 루프(특히 JSON 직렬화 시)에 빠지기 쉽습니다. 일단 모든 매핑을 '단방향'으로 끝낸 뒤, 나중에 반대 방향에서 조회가 꼭 필요한 경우에만 양방향(mappedBy)을 추가하는 것이 정석입니다.
2. 지연 로딩 (Lazy Loading)은 선택이 아닌 필수! 회원을 조회할 때마다 그 회원이 속한 팀 정보까지 매번 싹 다 DB에서 긁어오면 성능이 박살 납니다. 따라서 실무에서는 모든 연관관계에 무조건 **FetchType.LAZY (지연 로딩)**를 걸어둡니다. 이렇게 하면 회원만 딱 조회해 오고, 팀 객체는 **'프록시(가짜 객체)'**로 비워두었다가, 나중에 실제 member.getTeam().getName() 처럼 팀 데이터를 진짜 쓸 때 그제야 DB에 팀 조회 쿼리를 날립니다. (N+1 문제 해결의 첫걸음)
6. 방언(Dialect) 처리가 용이하여 DB 마이그레이션과 유지보수에 탁월함
데이터베이스(DB)의 세계에는 표준어인 ANSI SQL이 존재하지만, 각 DB 벤더사(MySQL, Oracle, PostgreSQL 등)마다 자신들만의 고유한 기능이나 문법을 가지고 있습니다. 이를 데이터베이스 **'방언(Dialect)'**이라고 부릅니다.
- (예: 페이징 처리 시 MySQL은 LIMIT을 쓰지만, Oracle은 ROWNUM을 사용함)
JPA의 찰떡같은 번역 기능: 과거에는 DB를 MySQL에서 Oracle로 바꾸려면(마이그레이션) 프로젝트 내의 수백, 수천 개의 쿼리문을 일일이 다 뜯어고쳐야 했습니다. 하지만 JPA를 사용하면 이 문제가 아주 우아하게 해결됩니다.
- 설정 한 줄의 마법: application.yml 같은 설정 파일에서 사용할 DB의 **Dialect(방언)**만 띡 지정해주면 끝입니다. (예: org.hibernate.dialect.MySQL8Dialect)
- 자동 번역: 개발자는 특정 DB에 종속되지 않고 JPA 표준 방식이나 JPQL로 코드를 짭니다. 그러면 JPA가 설정된 Dialect를 보고, 해당 DB에 딱 맞는 맞춤형 쿼리(SQL)로 알아서 번역해서 날려줍니다.
결과적으로 특정 데이터베이스 기술에 종속되지 않게 만들어주어, 훗날 서비스가 커져서 DB를 교체해야 할 때나 코드를 유지보수할 때 압도적인 편리함을 제공합니다.
1. 소켓(Socket)과 통신 프로토콜
1) 소켓(Socket)이란?
- 네트워크 상에서 두 컴퓨터가 데이터를 주고받기 위해 열어두는 **'통신 창구(문)'**입니다.
- 소켓을 열기 위해서는 **IP 주소(건물 주소), 포트 번호(방 번호), 프로토콜(통신 규칙)**이 필요합니다.
- 우리가 아는 모든 웹 통신은 결국 밑바닥에선 이 '소켓'을 열어서 데이터를 주고받는 것입니다.
2) HTTP 통신 vs 웹소켓(WebSocket) 통신
- HTTP 통신: * 단방향 / 비연결성(Stateless): 클라이언트가 질문(Request)하면 서버가 대답(Response)하고 전화를 뚝 끊어버립니다.
- 주로 HTML 문서나 JSON 데이터를 가볍게 주고받을 때 사용합니다.
- 웹소켓 (WebSocket):
- 양방향 / 연결 지향: 한 번 연결되면 전화를 끊지 않고 계속 파이프를 열어둡니다.
- 서버가 클라이언트에게 먼저 데이터를 보낼 수도 있습니다. (예: 실시간 채팅, 주식 호가창)
2. Web Server vs WAS (Tomcat)
- 웹 서버 (Web Server / ex: Apache, Nginx):
- 단순한 아르바이트생입니다. HTML, CSS, 이미지 같은 정적(Static) 파일을 요청받으면 그냥 그대로 던져주는 역할만 합니다. 연산 능력(컴파일)이 없습니다.
- WAS (Web Application Server / ex: Tomcat):
- 전문 요리사입니다. 웹 서버가 처리하지 못하는 동적(Dynamic) 요청(예: DB 조회해서 화면 그리기, .jsp 처리)을 넘겨받아 자바 코드를 실행하고 완성된 HTML을 만들어 웹 서버에게 돌려줍니다.
3. 서블릿(Servlet)과 스레드 풀(Thread Pool)
[정확한 서블릿 생명주기와 동작 방식]
- 서블릿(Servlet): 자바를 사용해 웹 요청을 처리할 수 있게 만든 클래스. (서블릿 컨테이너 = 톰캣이 이 서블릿들을 관리함)
- 싱글톤(Singleton): 서블릿 객체는 톰캣이 켜질 때(또는 최초 요청 시) 딱 1개만 생성되어 메모리에 계속 살아있습니다.
- 스레드 풀(Thread Pool): 1. 클라이언트의 요청이 들어옵니다. 2. 톰캣은 요청마다 서블릿 객체를 새로 만드는 게 아니라, 미리 만들어둔 '스레드(일꾼)' 하나를 스레드 풀에서 꺼내 배정합니다. 3. 이 스레드가 HttpServletRequest, HttpServletResponse 객체를 짐보따리처럼 만들어서, 이미 만들어져 있는 단 1개의 서블릿 객체에게 달려가 일을 시킵니다. 4. 일이 끝나면 스레드는 다시 스레드 풀로 돌아가 대기하고, Request/Response 객체는 소멸됩니다.
💡 왜 이렇게 할까요? 스레드나 객체를 무한정 생성하면 서버 메모리가 터집니다. 그래서 스레드 개수를 제한(예: 200개)해두고, 초과하면 대기열(Queue)에서 기다리게 하여 서버가 죽지 않고 빠른 속도를 유지하게 만듭니다.
4. URL vs URI
- URI (Uniform Resource Identifier): 인터넷 상의 자원을 식별하는 가장 큰 개념 (주민등록번호).
- URL (Uniform Resource Locator): 자원이 실제로 어디 있는지 알려주는 위치 정보 (집 주소).
- (참고: 우리가 흔히 웹 브라우저에 치는 주소는 URL이자 URI입니다. 요즘은 거의 동의어처럼 쓰입니다.)
🤔 Q&A: 그럼 실제 비즈니스 로직 처리는 서블릿 객체가 진행하는 것인지?
A. 과거에는 YES, 하지만 현재(스프링)는 NO입니다!
- 과거 (순수 Servlet 시절): LoginServlet, BoardServlet 등 서블릿 객체 안에다가 DB 접속하고 로직 짜고 HTML 문자열까지 다 박아 넣었습니다. (서블릿이 비즈니스 로직을 다 함)
- 현재 (스프링 프레임워크): 톰캣 안에는 **DispatcherServlet**이라는 대장 서블릿 딱 하나만 둡니다. 이 서블릿은 비즈니스 로직을 직접 처리하지 않습니다. 요청을 받으면 스프링 컨테이너 안에 있는 Controller -> Service로 "너네가 일해!" 하고 던져버립니다. 즉, 실제 비즈니스 로직은 스프링의 @Service 클래스들이 처리합니다.
5. web.xml 이란? (그리고 왜 지금은 안 보일까?)
1) web.xml의 정체 (한 줄 요약) 과거 톰캣(Web Container)이 애플리케이션을 켤 때 가장 먼저 읽어보던 **'초기 설정 매뉴얼'**입니다. "우리 건물(서버)은 이렇게 운영할 거야!"라는 규칙이 담긴 XML 문서입니다.
2) 딱 3가지만 기억하는 핵심 역할 과거에는 여기에 수십 가지 설정을 적었지만, 핵심은 다음 세 가지였습니다.
- 문지기 고용 (DispatcherServlet 매핑): "클라이언트의 모든 요청은 스프링의 프론트 컨트롤러(DispatcherServlet)가 받게 해!"라고 연결해 줍니다.
- 보안 검색대 설치 (Filter 설정): 요청이 문지기에게 닿기 전에 한글 인코딩을 맞추거나, 신분을 확인하는 필터를 설치합니다.
- 비상구 설정 (Error Page): 404(페이지 없음), 500(서버 에러) 발생 시 보여줄 예쁜 에러 화면의 경로를 지정합니다.
3) 💡 실무 포인트: 그럼 스프링 부트에서는 어디로 갔을까? 과거(Spring Legacy) 개발자들은 프로젝트를 시작할 때 이 web.xml을 세팅하느라 진을 뺐습니다. 하지만 **스프링 부트(Spring Boot)**가 등장하면서, 내장 톰캣이 도입되고 이 모든 XML 설정들이 자바 코드(어노테이션)와 application.yml 설정으로 전부 자동화되었습니다.
즉, 우리는 더 이상 web.xml을 직접 만질 일이 없지만, **"스프링 부트가 뒤에서 알아서 이 매뉴얼을 세팅해 주고 있구나"**라고 원리를 이해하는 것이 중요합니다.
앞으로 할 것
AOP
PSA
'개발 공부 > 스프링' 카테고리의 다른 글
| 순수 Java로 WAS 구현해보기 (1) - 스프링과 WAS의 핵심 개념 이해 (1) | 2026.04.06 |
|---|---|
| SpringApplication에 대한 정리 (0) | 2026.03.25 |
| 백엔드 계층 구조 정리 2 (0) | 2026.03.11 |
| 백엔드 계층 구조 정리 1 (0) | 2026.03.11 |
| 스프링 기초 (0) | 2026.02.17 |