본문 바로가기
Develop/Study

Spring Framework

by ys2ys2 2025. 3. 14.

Spring Framework란

 

Java 엔터프라이즈 애플리케이션 개발을 위한 오픈소스 애플리케이션 프레임워크로

객체 지향 프로그래밍(OOP) 원칙을 따르면서, 개발자가 효율적이고 확장 가능한 애플리케이션을 만들 수 있도록 지원하는 것이다.

 


 

Spring Container

 

 

스프링에서 객체(Bean)들을 생성하고 관리하는 컨테이너다.

모든 객체를 개발자가 직접 생성하는 것이 아니라, Spring Container가 관리한다.

컨테이너는 ApplicationContext 인터페이스를 통해 제공된다.

 

스프링에서 객체를 생성한다는 것은 클래스를 메모리에 올려(new 키워드) 인스턴스를 만들고 객체를 관리하는 것을 의미하는데

Spring Container가 객체를 생성하는 것은 일반적으로 개발자가 new 키워드를 사용하여 직접 생성하는 것과는 다르게 Spring이 대신 객체를 생성하고 관리하는 방식이다.

 

객체란 클래스로부터 만들어진 인스턴스를 의미하는데 데이터(필드)와 행동(메서드)를 포함하는 독립적인 실행 단위다.

 

일반적인 객체 생성은 new 키워드를 사용하는데 (new UserService() )

이렇게 생성한 객체는 직접 관리해야 하며, 여러 곳에서 재사용하려면 매번 new 키워드를 사용하여 새 인스턴스를 만들어야 한다.

public class UserService {
    public void getUser() {
        System.out.println("사용자 정보 가져오기");
    }
}

public class MainApp {
    public static void main(String[] args) {
        UserService userService = new UserService(); // 객체 생성 (new 키워드 사용)
        userService.getUser(); // 메서드 실행
    }
}

 

그러나 Spring에서는 객체를 직접 생성하는 것이 아니라, 컨테이너(Spring Container)가 객체를 대신 생성하고 관리한다.

Service 어노테이션을 주입하여 Spring Container가 UserService 객체를 자동으로 생성하고

Autowired 어노테이션으로 Spring 컨테이너에서 생성된 객체(Dependency injection, DI)를 주입해준다.

@Service  // Spring이 자동으로 객체를 생성하도록 선언
public class UserService {
    public void getUser() {
        System.out.println("사용자 정보 가져오기");
    }
}

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired  // Spring이 자동으로 객체를 주입(DI 적용)
    private UserService userService;

    @GetMapping("/info")
    public String getUserInfo() {
        userService.getUser();
        return "사용자 정보 호출 완료";
    }
}

 

이렇게 Spring 컨테이너가 객체의 생명 주기를 관리하고, 필요할 때만 객체를 생성하여 메모리를 절약할 수 있다.

또한 객체를 여러 곳에서 사용할 때 싱글톤으로 관리되므로 불필요한 객체 생성을 방지할 수 있고 유지보수도 편리해진다.

 


DI(Dependency Injection, 의존성 주입)

 

DI는 객체 간의 의존성을 외부에서 주입하는 방식을 의미한다.

@Autowired, @inject, @Resource 등을 사용하여 Spring Container가 필요한 객체를 자동으로 주입해준다.

 

이런 생성자 주입 방식을 사용하면 객체 간의 결합도를 낮출 수 있어 유지보수가 쉬워진다.

 


관점 지향 프로그래밍(AOP, Aspect-Oriented Programming)

 

 

AOP는 핵심 비즈니스 로직과 공통 기능(로깅, 보안, 트랜잭션 등)을 분리하는 개념이다.

AOP를 사용하면 공통 관심사를 하나의 코드(Aspect)로 작성하여 재사용 할 수 있다. 또한 한 곳에서 수정하면 모든 메서드에 적용되므로 유지보수가 쉬워진다.

 

AOP 미적용 방식

OrderService

@Service
public class OrderService {

    public void placeOrder() {
        long startTime = System.currentTimeMillis(); // 실행 시간 측정 시작

        System.out.println("주문을 처리하는 중...");
        try {
            Thread.sleep(1000); // 주문 처리
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis(); // 실행 시간 측정 종료
        System.out.println("메서드 실행 시간: " + (endTime - startTime) + "ms");
    }
}

 

다른 서비스에서도 동일한 실행 시간 측정 코드 반복 PaymentService

@Service
public class PaymentService {

    public void processPayment() {
        long startTime = System.currentTimeMillis();

        System.out.println("결제 처리 중...");
        try {
            Thread.sleep(500); // 결제 처리
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("메서드 실행 시간: " + (endTime - startTime) + "ms");
    }
}

 

이렇게 되면 startTime과 endTime을 이용한 실행 시간 측정 코드가 여러 서비스 클래스에 반복되므로 중복 코드가 발생하며, 로직이 변경될 경우 모든 서비스에서 일일이 수정해야 하는 문제점이 생긴다.

 

 

AOP 적용 방식

공통 관심사인 메서드 실행 시간 측정을 분리해서 한 곳에서 관리하기

 

Aspect 어노테이션으로 AOP 기능을 제공하는 클래스임을 선언하고

Component 어노테이션으로 Spring Bean에 등록해서 자동 관리되게 한다.

Around 어노테이션으로 패키지 내의 모든 메서드 실행 시간 측정을 적용한다.

@Aspect
@Component
public class ExecutionTimeAspect {

    @Around("execution(* com.example.service.*.*(..))") // 모든 Service 메서드에 적용
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis(); // 실행 시간 측정 시작
        Object result = joinPoint.proceed(); // 실제 메서드 실행
        long executionTime = System.currentTimeMillis() - start; // 실행 시간 계산

        System.out.println(joinPoint.getSignature() + " 실행 시간: " + executionTime + "ms");
        return result;
    }
}

 

@Service
public class OrderService {
    public void placeOrder() {
        System.out.println("주문을 처리하는 중...");
        try {
            Thread.sleep(1000); // 주문 처리
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

@Service
public class PaymentService {
    public void processPayment() {
        System.out.println("결제 처리 중...");
        try {
            Thread.sleep(500); // 결제 처리
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

AOP를 적용하면 placeOrder(), processPayment()메서드에는 실행 시간 측정 코드가 포함되지 않게 된다.

ExecutionTimeAspect 클래스에서 공통의 관심사를 관리하므로 유지보수가 훨씬 편리해진다.

또한 새로운 서비스가 추가되어도 AOP가 자동 적용되므로, 실행 시간 측정 기능을 별도로 추가할 필요가 없다.

 


 

서비스 추상화(PSA, Portable Service Abstraction)

 

 

PSA란 Spring이 다양한 기술(JDBC, JPA, MyBatis, Cache, Security 등)을 하나의 추상화 계층을 통해 일관된 방식으로 사용할 수 있도록 제공하는 개념이다.

 

기존에는 JDBC, Hibernate, MyBatis 같은 데이터 접근 기술을 직접 구현해야 했고, 기술이 변경되면 코드 전체를 수정해야 하는 문제가 발생했지만 PSA를 사용하면 기술이 변경되더라도 인터페이스를 유지하면서 쉽게 교체가 가능해진다.

 

MyBatis

@Repository
public class UserDAO {
    @Autowired
    private UserMapper userMapper;

    public User findUserById(int id) {
        return userMapper.selectUserById(id);
    }
}

 

JPA

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
}

 

MyBatis에서 JPA로 변경했어도 Service 계층의 코드는 변경하지 않아도 된다. Spring이 기술을 추상화(PSA) 했기 때문에 특정 기술을 변경하더라도 코드 변경이 최소화 된다.

 


 

트랜잭션 관리(@Transactional)

 

 

트랜잭션은 데이터베이스의 작업을 하나의 논리적인 단위로 묶어 처리하는 개념이다.

모든 작업이 완벽히 수행되거나 하나라도 실패하면 모든 작업을 되돌려야 한다.(원자성)

 

1. A 계좌에서 10만 원 출금
2. B 계좌에 10만 원 입금
3. 데이터 저장 (DB Commit)

 

이라는 단계가 있을 때

3번 과정에서 오류가 발생하면 1번 과정에서 이미 돈이 빠져나갔으므로 데이터 불일치가 발생한다.

트랜잭션이 적용되어 있으면 모든 작업이 성공해야지만 DB에 저장되며 실패 시 이전 상태로 롤백이 된다.

 

트랜잭션 적용 전

@Service
public class BankService {

    @Autowired
    private AccountRepository accountRepository;

    public void transferMoney(Long fromAccountId, Long toAccountId, int amount) {
        Account fromAccount = accountRepository.findById(fromAccountId).get();
        Account toAccount = accountRepository.findById(toAccountId).get();

        fromAccount.withdraw(amount); // 출금
        toAccount.deposit(amount);    // 입금

        accountRepository.save(fromAccount); 
        accountRepository.save(toAccount);   // 여기서 오류 발생하면 데이터 불일치 문제 발생
    }
}

 

accountRepository.save(toAccount)에서 예외 발생시 A계좌에서 돈은 빠졌지만 B계좌에서는 입금되지 않는 문제가 발생한다.

 

트랜잭션 적용 후

@Service
public class BankService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional  // 트랜잭션 적용 (Rollback 보장)
    public void transferMoney(Long fromAccountId, Long toAccountId, int amount) {
        Account fromAccount = accountRepository.findById(fromAccountId).get();
        Account toAccount = accountRepository.findById(toAccountId).get();

        fromAccount.withdraw(amount); 
        toAccount.deposit(amount);

        accountRepository.save(fromAccount); 
        accountRepository.save(toAccount);
    }
}

 

Transactional 어노테이션을 추가하면 하나라도 작업이 실패할 시 모든 작업이 취소(Rollback) 되므로 데이터 불일치 문제가 해결된다.

fromAccount.withdraw(amount);가 실행되었어도, accountRepository.save(toAccount);에서 예외가 발생하면 자동으로 롤백됨.

 


 

Spring MVC

 

 

Java 웹 개발에서는 클라이언트 요청을 받고 데이터를 가공해서 응답하는 과정이 복잡할 수 있는데

Spring MVC는 Controller, Service, Repository 계층을 나누어 구조적으로 개발할 수 있도록 지원하여 요청과 응답을 체계적으로 관리할 수 있다.

또한 비즈니스 로직과 데이터 처리를 분리하여 확장성과 유지보수성이 뛰어나고, 다양한 응답 형식(JSON, HTML)을 쉽게 처리할 수 있다.

 

Model - View - Controller = MVC

DispatcherServlet을 중심으로 Controller, Service, Repository 계층을 나누어 구현.

클라이언트 요청 → DispatcherServlet → Controller(view-jsp,html) → Service → Repository → DB

 

사용자가 웹 브라우저에서 URL을 입력하면 DispatcherServlet이 요청을 받고

DispatcherServlet이 Controller를 찾아 실행한다.

Controller에서는 비즈니스 로직인 Service와 데이터를 처리하는 Repository를 호출하고

최종 데이터를 받아 View(JSP, HTML) 또는 JSON 형태로 클라이언트에 응답한다.

 

 


 

REST API

 

 

REST(Representational State Transfer)는 자원을 리소스(Resource)로 정의하고 HTTP 프로토콜을 사용하여 상태를 주고받는 아키텍처 스타일

REST API는 이 REST 원칙을 따르는 API(응용 프로그램 인터페이스) 를 의미

 

REST API의 주요 원칙

  • 클라이언트-서버 구조
  • Stateless(무상태성)
  • Cacheable(캐시 가능성)
  • Uniform Interface(일관된 인터페이스)
  • Layered System(계층화 구조)

클라이언트-서버 구조 : 클라이언트와 서버가 분리되어 독립적으로 동작해야 함.

무상태성 : 서버는 클라이언트의 요청을 받을 때마다 독립적으로 처리해야 함.

캐시 가능성 : API 응답을 캐싱하여 성능을 최적화할 수 있어야 함.

일관된 인터페이스 : URI 설계가 일관된 방식으로 리소스를 표현해야 함

계층화 구조 : API서버는 보안, 로드 밸런싱 등을 위한 중간 계층을 추가할 수 있어야 함

 

REST API의 특징

  • 클라이언트-서버 구조
  • Stateless(무상태성)
  • 리소스 기반 설계
  • HTTP 메서드 활용
  • JSON 응답

리소스 기반 설계 : URL를 활용하여 자원을 명확하게 표현

HTTP 메서드 활용 : GET, POST, PUT, DELETE 등의 HTTP 메서드를 사용하여 CRUD 작업 수행

JSON 응답 : 데이터 교환 방식으로 JSON을 주로 사용

 

REST API 설계 시 주의할 점

URI는 명사로 표현

GET /getUser/1 ❌ → GET /users/1 ✅

POST /createUser ❌ → POST /users ✅

 

복수형을 사용하여 일관된 리소스 표현

GET /user/1 ❌ → GET /users/1 ✅

 

예외 처리를 적절히 해야 함(존재하지 않는 데이터를 요청하면 404 반환)