KDT:::Sparta MSA 아키텍처 3기(week02)

8 minute read




스프링의 핵심 원리

강한결합의 문제점

  • 코드 내에서 직접 new를 사용해 객체를 생성
  • 하나의 변경이 다른 코드의 변경을 유발하는 구조는 유지보수가 어렵다.
  • 모듈간 의존성이 높아 테스트가 힘들다.

    Spring IoC 컨테이너와 IoC로 해결

    Spring IoC - 애플리케이션을 구성하는 객체(Bean)들의 생성, 관리, 연결을 총괄

  • 객체 생성 및 관리 : 어노테이션을 읽어 객체를 생성하고 소멸까지 생성주기를 관리
  • 의존성 연결 : 객체간의 관계를 파악하여 알맞게 연결

    IoC (Inversion of Control) - 제어의 역전

  • Spring 컨테이너가 동작하는 핵심원리
  • 과거에는 개발자가 직접 객체를 생성하고 제어했지만, 이제는 그 ‘제어권’을 Spring 컨테이너에게 역전(Inversion)시킴.

    Spring Bean - 컨테이너가 관리하는 객체의 단위

  • Spring IoC 컨테이너가 직접 생성하고, 의존관계를 설정해주며, 생명주기까지 관리하는 객체
  • 클래스에 @Component, @Service, @Repository, @Controller 등의 어노테이션을 붙인다.
  • Spring이 애플리케이션 시작 시 해당 클래스를 스캔하여 IoC 컨테이너에 Bean으로 생성(등록)한다.
  • @Component는 가장 기본적인 어노테이션이며, 나머지는 특정 목적을 가진 특화된 형태이다.

    DI (Dependency Injection) - IoC를 구현하는 기술

  • 클래스가 필요로 하는 의존 객체(Dependency)를 Spring 컨테이너가 자동으로 찾아서 연결(주입, Injection)해준다.
  • 강한결합, DI 코드비교
// 강한 결합의 예시
public class UserController {
    // UserService가 UserRepository의 존재를 직접 알고, 직접 생성함
    private final UserService userService = new UserServiceImpl();
   // UserServiceImpl를 NewUserServiceImpl로 변경해야 한다면, UserController 클래스의 코드를 직접 수정해야 한다.

    public void save() {
        userService.save();
    }
}

// DI를 통해 느슨한 결합이 구현된 예시
public class UserController {
   
   //'어떤' Service인지는 모르지만, UserService타입의 Bean이 필요하다고 선언만 함
    private final UserService userService;


   // 생성자를 통해 Spring 컨테이너가 알아서 적절한 Bean을 '주입'해준다.
    public UserController(UserService userService) {
        this.userService = userService;
    }
}

// 스프링컨테이너 빈 등록 
@Configuration
public class BeanConfig {

  @Bean
  public UserService userService() {
    return new UserServiceImpl(); // UserServiceImpl를 NewUserServiceImpl로 바꾸고 싶어도, UserService의 코드는 단 한 줄도 수정할 필요가 없어진다.
  }
}



의존성 주입(DI)의 3가지 방식

생성자주입 - UserController를 만들 때, 필요한 부품(UserService)을 생성자에서 바로 넣어줌.

  • 📘 개념
    • 가장 권장되는 방식
    • 의존 객체를 생성자 매개변수로 전달받는 방식
    • final 키워드와 함께 쓰면 불변성을 보장할 수 있다.
  • ✅ 예시
    @Component
    public class UserController {
    
      private final UserService userService; // final로 선언
    
      // 생성자 주입
      public UserController(UserService userService) {
          this.userService = userService;
      }
    
      public void getUser() {
          userService.findUser();
      }
    }
    
  • 🌟 장점
    • 불변성 보장: final로 선언해 한 번 주입된 후 변경 불가
    • 필수 의존성 보장: 생성자 호출 시 반드시 주입되어야 하므로 NPE 위험 낮음
    • 테스트 용이: 직접 mock 객체를 생성자에 주입할 수 있음
    • 순환 참조 감지 가능: 스프링이 컴파일 시점에 감지 가능
  • ⚠️ 단점
  • 의존성이 너무 많을 때(예: 5개 이상) 생성자가 길어질 수 있음

    수정자주입 - UserController를 먼저 만들고, 나중에 필요한 부품을 세터 메서드로 꽂아줌.

  • 📘 개념
    • 의존 객체를 setter 메서드(수정자) 를 통해 주입받는 방식
    • 기본 생성자로 객체를 만들고, 나중에 setUserService() 같은 메서드로 주입
  • ✅ 예시
    @Component
    public class UserController {
    
      private UserService userService; 
    
      /* setter 메서드를 통해 주입 
        👉 객체가 만들어진 다음에
        👉 setUserService()라는 메서드를 이용해서
        👉 외부(Spring 컨테이너)가 의존 객체를 집어넣어주는 것
      */
      @Autowired
      public void setUserService(UserService userService) {
          this.userService = userService; // 객체가 만들어진 뒤에 값이 들어오므로 private final UserService userService; 사용불가.
      }
    
      public void getUser() {
          userService.findUser();
      }
    }
    
  • 🌟 장점
    • 선택적 의존성을 주입할 때 유용 (필수 아님)
    • 순환 의존성 문제 해결에 유리한 경우 있음
  • ⚠️ 단점
    • 객체가 완전히 생성된 이후에 의존성이 주입되므로, 주입이 누락되면 런타임 오류 발생 가능
    • 불변성 보장 불가 (필드를 final로 못 씀)
    • 코드가 장황해질 수 있음

      필드주입

  • 📘개념
    • 의존 객체를 필드에 직접 주입하는 방식
    • @Autowired를 필드에 바로 붙입니다.
    • 예전 코드나 간단한 테스트용 코드에서 자주 사용됩니다.
  • ✅ 예시
    @Component
    public class UserController {
    
      @Autowired
      private UserService userService; // 필드에 직접 주입
    
      public void getUser() {
          userService.findUser();
      }
    }
    
  • 🌟 장점
    • 코드가 간결하고 빠르게 작성 가능
    • 테스트나 간단한 예제에서 편리
  • ⚠️ 단점
    • 의존 관계가 외부에서 보이지 않음 (캡슐화 깨짐)
    • 불변성 보장 불가
    • 순환 의존성 감지 어려움
    • 단위 테스트 시 Mock 객체 주입 어려움 ➡️ 그래서 실무에서는 비추천

총정리

구분 생성자 주입 수정자 주입 필드 주입
주입 시점 객체 생성 시 객체 생성 후 런타임 시
불변성 ✅ 보장 ❌ 불가 ❌ 불가
필수 의존성 보장 ✅ 가능 ❌ 불가 ❌ 불가
코드 가독성 약간 복잡 보통 간결
테스트 용이성 ✅ 매우 높음 보통 ❌ 낮음
순환참조 감지 ✅ 컴파일 시 ⚠️ 제한적 ❌ 어려움
스프링 권장 여부 ✅ 적극 권장 보조적 사용 ❌ 비추천



계층형 아키텍처 (Layered Architecture)

  • Client ↔ Controller ↔ Service ↔ Repository ↔ DB
    • Controller: 클라이언트의 HTTP 요청을 가장 먼저 받는 ‘진입점’ 요청을 분석하여 적절한 서비스 계층의 메서드를 호출하고, 그 결과를 클라이언트에게 응답하는 역할
    • Service: 애플리케이션의 핵심 ‘비즈니스 로직‘을 처리 데이터 가공, 트랜잭션 관리 등 실제 업무 규칙이 구현되는 곳
    • Repository: 데이터베이스와 직접 통신하는 ‘데이터 접근 계층’ JPA의 JpaRepository를 상속받아 데이터의 저장, 조회, 수정, 삭제를 담당



기본적인 Spring Boot 폴더 구조 (권장 패키지 구조)

구분 파일명 역할
Controller ProductController.java HTTP 요청을 받아 서비스 호출, 결과 반환
Service ProductService.java 비즈니스 로직 담당 (예: 검증, 계산, 트랜잭션 처리 등)
Repository ProductRepository.java 데이터베이스와 직접 통신 (JPA 인터페이스)
Entity Product.java DB 테이블과 1:1 매핑되는 클래스 (필드 = 컬럼)
Main DemoApplication.java @SpringBootApplication 붙은 시작 클래스
Config (선택) WebConfig.java, BeanConfig.java Bean 설정, CORS, 인터셉터 등 추가 설정
src
 └─ main
     ├─ java
     │   └─ com.example.demo
     │       ├─ DemoApplication.java          ← 메인 클래스 (프로그램 시작점)
     │       │
     │       ├─ controller                    ← 요청(Request)을 받는 계층
     │       │    └─ ProductController.java   ← @RestController 작성 위치
     │       │
     │       ├─ service                       ← 비즈니스 로직 계층
     │       │    └─ ProductService.java
     │       │
     │       ├─ repository                    ← DB 접근 계층 (JPA 등)
     │       │    └─ ProductRepository.java
     │       │
     │       └─ entity                        ← DB 테이블과 매핑되는 도메인 객체
     │            └─ Product.java
     │
     └─ resources
         ├─ application.yml                   ← 설정 파일
         └─ static / templates (선택)         ← 정적 파일 or 템플릿



RESTful API 설계

RESTful 등장 전, RPC(Remote Procedure Call) 스타일이나 SOAP(Simple Object Access Protocol) 같은 방식으로 통신

  • RPC 스타일: /getUser, /deleteProduct처럼 URI에 행위(동사)가 포함된 방식
    • 서버의 특정 함수를 그대로 호출하는 것과 같아, 클라이언트와 서버가 서로의 내부 구현에 강하게 결합(Tight Coupling)되는 문제가 있었습니다.
  • SOAP 프로토콜: XML을 기반으로 한 매우 복잡하고 무거운 프로토콜
    • 규칙이 엄격하고, 메시지 구조가 방대하여 배우기 어렵고 오버헤드가 큼

      🌐 RESTful API 핵심 원칙

      RESTful API는 단순히 HTTP를 사용하는 것이 아니라,
      리소스를 일관된 방식으로 표현하고 조작하는 아키텍처 스타일입니다.


🧩 1️⃣ Uniform Interface (일관된 인터페이스)

모든 자원(Resource)에 대해 일정한 규칙으로 접근해야 합니다.

  • 자원은 URI로 표현합니다.
  • GET /users → 사용자 목록 조회
  • GET /users/1 → 특정 사용자 조회
  • POST /users → 사용자 생성
  • PUT /users/1 → 사용자 전체 수정
  • DELETE /users/1 → 사용자 삭제
  • HTTP 메서드(GET, POST, PUT, DELETE, PATCH 등)를 자원 조작 의미로 사용
  • 명사 중심의 URI, 일관성 있는 구조 유지

    효과: 클라이언트가 API 구조를 쉽게 예측하고 일관된 방식으로 사용할 수 있음


🚫 2️⃣ Stateless (무상태성)

서버는 클라이언트의 상태(Session, Context)를 유지하지 않습니다.

  • 요청마다 필요한 모든 정보를 포함해야 합니다. (예: 인증 토큰, 파라미터 등)
  • 서버는 이전 요청을 기억하지 않고, 각 요청은 독립적으로 처리됩니다.

    효과: 서버 확장성(Scalability) 향상, 부하 분산 용이


👩‍💻 3️⃣ Client–Server 구조 분리

클라이언트(프론트엔드)와 서버(백엔드)의 역할을 명확히 분리합니다.

  • 클라이언트 → UI/UX 담당
  • 서버 → 데이터와 비즈니스 로직 담당
  • 서로 독립적으로 개발·배포 가능

    효과: 유지보수성 향상, 다양한 플랫폼(웹/모바일)에서 동일한 API 사용 가능


⚡ 4️⃣ Cacheable (캐시 가능성)

응답 데이터에 캐시 정책을 명시해 재사용할 수 있어야 합니다.

  • HTTP 헤더(Cache-Control, ETag, Last-Modified)를 활용
  • 변경이 적은 리소스(예: 이미지, 상품 목록)는 캐싱 가능

    효과: 네트워크 트래픽 감소, 응답 속도 향상


🧱 5️⃣ Layered System (계층화 구조)

클라이언트는 서버 내부 구조(계층) 를 몰라도 됩니다.

  • 요청은 여러 중간 서버(프록시, 로드 밸런서, 인증 서버 등)를 거칠 수 있음
  • 각 계층은 독립적으로 구성 가능

    효과: 보안 강화, 확장성 및 관리 용이성 향상


💾 6️⃣ Code on Demand (선택적)

서버가 클라이언트에게 실행 가능한 코드(JavaScript 등) 를 내려보낼 수 있습니다.
하지만 보안 및 제약 문제로 실제 REST API에서는 거의 사용되지 않습니다.


⚙️ 요약 표

원칙 의미 핵심 포인트
Uniform Interface 일관된 자원 표현 URI는 명사, HTTP 메서드로 동작 구분
Stateless 무상태성 요청마다 필요한 모든 정보 포함
Client–Server 역할 분리 UI와 비즈니스 로직 분리
Cacheable 캐시 가능 응답에 캐시 정책 명시
Layered System 계층 구조 프록시, 게이트웨이 등 중간 계층 허용
Code on Demand (선택) 실행 코드 전송 실제로는 거의 사용되지 않음

💡 정리:
RESTful API는

  • “리소스(자원)” 중심으로
  • “HTTP 메서드”를 활용해
  • “일관되고 상태 없는 방식”으로 설계된 API입니다.



#JPA 영속성 컨텍스트

  • 엔티티(Entity)를 영구 저장하는 환경이라는 뜻
  • 실제로는 애플리케이션과 데이터베이스 사이에서 엔티티를 담아두고 관리하는 논리적인 공간(메모리 캐시)
  • EntityManager를 통해 이 공간에 접근하고 관리

    엔티티의 4가지 상태 (Lifecycle)

  • 비영속
  • 영속
  • 준영속
  • 삭제

    장점

  • 1차캐시
  • 동일성보장
  • 쓰기지연
  • 변경감지



트랜잭션

  • 트랜잭션은 이 두 작업을 ‘하나의 논리적인 작업 단위’로 묶어, “두 작업이 모두 성공하거나, 하나라도 실패하면 모두 없던 일로 만든다(롤백)”는 것을 보장
  • 이를 통해 어떤 상황에서도 데이터의 일관성과 무결성이 지켜진다.

    @Transactional

  • 동작 원리: AOP와 프록시(Proxy)
    • @Transactional이 붙은 메서드를 호출하면, Spring은 프록시(Proxy)라는 가상의 대리 객체를 통해 해당 메서드를 실행.
    • 프록시의 역할:
      1. 메서드 실행 전, 데이터베이스에 트랜잭션을 시작 (BEGIN TRANSACTION)
      2. 실제 메서드 로직을 실행
      3. 메서드가 예외 없이 성공적으로 종료되면, 트랜잭션을 커밋(Commit)하여 모든 변경사항을 DB에 영구 저장
      4. 메서드 실행 중 RuntimeException 등 예외가 발생하면, 트랜잭션을 롤백(Rollback)하여 모든 변경사항을 취소하고 원래 상태로 되돌린다.
@Service
@RequiredArgsConstructor
public class PurchaseService {
    private final PurchaseRepository purchaseRepository;
    private final ProductRepository productRepository;

    @Transactional
    public void placeOrder(OrderRequest orderRequest) {
        // 1. 주문 생성 로직 (DB 변경 1)
        Purchase purchase= new Purchase(purchaseRequest);
        purchaseRepository.save(order);
    
        // 2. 재고 감소 로직 (DB 변경 2)
        Product product = productRepository.findById(orderRequest.getProductId())
            .orElseThrow(() -> new EntityNotFoundException("상품 없음"));
        product.decreaseStock(orderRequest.getQuantity());

        // 3. 만약 재고가 음수가 되면 예외 발생
        if (product.getStock() < 0) {
            throw new IllegalArgumentException("재고가 부족합니다.");
        }
    }
}
  • Checked Exception에 대해서는 롤백하지 않으므로, 필요시 rollbackFor 옵션을 사용해야 한다.



Dirty Checking과 Flush

Dirty Checking 이란?

  • 영속성 컨텍스트가 관리하는 엔티티의 변경 사항을 감지하여, UPDATE SQL을 자동으로 생성하고 실행해주는 JPA의 매우 강력하고 핵심적인 기능
  • 동작 원리: 스냅샷과의 비교
  • JPA는 “스냅샷” 비교를 통해 이 ‘마법’ 같은 기능을 구현합니다.
      1. 조회 및 스냅샷 생성: 트랜잭션 안에서 엔티티를 처음 조회하면(findById()), JPA는 해당 엔티티를 1차 캐시에 저장할 때 그 상태 그대로의 복사본(스냅샷)을 함께 저장.
      1. 엔티티 상태 변경: 개발자가 코드에서 setter 등을 호출하여 영속 상태의 엔티티 필드 값을 변경. 이 변경은 우선 메모리에 있는 객체에만 반영된다.
      1. 변경점 비교: 트랜잭션 커밋 등으로 Flush가 호출되는 시점에, JPA는 1차 캐시에 있는 모든 엔티티의 현재 상태와 최초에 저장해둔 스냅샷을 필드별로 일일이 비교한다.
      1. UPDATE 쿼리 자동 생성: 만약 두 상태가 다르다면(즉, 변경이 감지되면), JPA는 변경된 내용을 바탕으로 UPDATE 쿼리를 자동으로 생성하여 ‘쓰기 지연 SQL 저장소’에 등록한 뒤 데이터베이스에 전송

        Flush란?

  • 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업
  • ‘쓰기 지연 SQL 저장소’에 쌓여있던 SQL 쿼리들을 실제 DB로 전송한다.