[Spring] 객체 지향과 Spring

Yuri·2025년 2월 5일

Spring

목록 보기
9/21

객체 지향 설계

SOLID 원칙

  1. 단일 책임 원칙 SRP (Single Responsibility Principle)

    • 하나의 클래스는 하나의 책임만 가져야 한다.
      • 클래스는 한 가지 기능에 집중해야 하며, 그 외의 기능을 담당하지 않아야 한다.
  2. 개방 폐쇄 원칙 OCP (Open Closed Principle)
    소프트웨어 요소는 확장에는 열려있어야 하고, 수정에는 닫혀있어야 한다.
    새로운 기능을 추가할 때 기존 코드를 수정하지 않고, 확장할 수 있도록 설계해야 한다.

    • 다형성을 활용하여 해결한다.
      • 인터페이스를 implements 하여 구현한 새로운 클래스를 만들어서 새로운 기능을 구현한다.
    public interface Shape {
        double calculateArea();
    }
    
    public class Circle implements Shape {
        public double calculateArea() { return /* 원의 넓이 계산 */; }
    }
    
    public class Square implements Shape {
        public double calculateArea() { return /* 사각형의 넓이 계산 */; }
    }
    
    public class AreaCalculator {
        public double calculate(Shape shape) {
            return shape.calculateArea();
        }
    }

    새로운 도형이 추가되더라도 인터페이스를 implements 하면 된다.
    AreaCalculator 를 수정할 필요가 없다.

  • 문제점

    // Circle을 계산하는 경우
    public class Main {
            public static void main(String[]) {
    
                    AreaCalculator areaCalculator = new AreaCalculator();
                    Circle circle = new Circle();
    
                    areaCalculator.calculate(circle);
    
            }
    }
    
    // Square를 계산하는 경우
    public class Main {
            public static void main(String[]) {
    
                    AreaCalculator areaCalculator = new AreaCalculator();
                    // Circle circle = new Circle();
                    Square square = new Square();
    
                    areaCalculator.calculate(square);
    
            }
    }

    구현 객체를 변경(Circle → Square)하기 위해서는 해당 코드를 사용하는 클라이언트측의 코드를 변경해야 한다. (위 코드에서는 main 메서드)
    👉 객체의 생성, 사용 등을 자동으로 설정해주는 ###가 필요하다.

  1. 리스코프 치환 원칙 LSP (Liskov Substitution Principle)

    • 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.
      • 부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 한다.
  2. 인터페이스 분리 원칙 ISP (Interface Segregation Principle)

    • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
      • 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
      • 즉, 하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스로 분리해야 한다.
  3. 의존관계 역전 원칙 DIP (Dependency Inversion Principle)

    • 구체적인 클래스에 의존하지 말고, 인터페이스나 추상 클래스에 의존하도록 설계해야 한다.
    // 알림 인터페이스(추상화)
    interface Notifier {
        void send(String message);
    }
    
    // Email 알림 클래스
    class EmailNotifier implements Notifier {
        @Override
        public void send(String message) {
            System.out.println("Email 알림: " + message);
        }
    }
    
    // SMS 알림 클래스
    class SMSNotifier implements Notifier {
        @Override
        public void send(String message) {
            System.out.println("SMS 알림: " + message);
        }
    }
    
    // 알림 서비스 (높은 수준 모듈)
    class NotificationService {
            // 추상화된 인터페이스에 의존
        private Notifier notifier;
    
        // 의존성 주입 (생성자를 통해 주입)
        public NotificationService(Notifier notifier) {
            this.notifier = notifier;
        }
    
        public void sendNotification(String message) {
                // notifier가 어떤 구현체인지 상관하지 않음
            notifier.send(message);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            // Email 알림을 사용
            Notifier emailNotifier = new EmailNotifier();
            NotificationService emailService = new NotificationService(emailNotifier);
            emailService.sendNotification("안녕하세요! 이메일 알림입니다.");
    
            // SMS 알림을 사용
            Notifier smsNotifier = new SMSNotifier();
            NotificationService smsService = new NotificationService(smsNotifier);
            smsService.sendNotification("안녕하세요! SMS 알림입니다.");
        }
    }
  • 필요한 Notifier 객체를 외부에서 주입받는다.
    • NotificationService는 어떤 알림 방식을 사용할지에 대한 세부 사항을 몰라도 되므로, 의존성이 약해진다.
      👉 Notifier 객체를 사용하는 쪽에서 수정이 필요하다.
  • 모듈간의 결합도를 낮추고 유연성과 확장성을 높일 수 있다.

    ✨ 객체 지향의 핵심은 "다형성"에 있다. 하지만 다형성 만으로는 OCP, DIP를 지킬 수 없다.

Spring과 객체 지향

Spring은 다형성 만으로는 해결하지 못했던 객체 지향 설계 OCP, DIP를 IOC, DI를 통해 가능하도록 만들어준다.

  • Spring의 역할
    • OCP, DIP 원칙을 지킬 수 있도록 도와준다.
    • 코드의 변경 없이 기능을 확장할 수 있도록 만들어준다.
    • 개발자가 마치 레고 블록을 조립하듯이 원하는 구성 요소를 손쉽게 교체하고 결합할 수 있도록 만들어준다.

💡 실무에서는 추상화 과정에서 비용(시간)이 발생하기 때문에 기능을 확장할 가능성이 없다면 구현 클래스를 직접 사용하고 추후 변경된다면 인터페이스로 리팩토링 한다.

Spring

Spring Container

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

  • Spring Container 의 역할
    • 객체(Bean)를 생성 및 관리하고 의존성을 주입하는 역할을 담당한다.
    • Spring Container를 사용하면 인터페이스에만 의존하는 설계가 가능해진다.
      • OCP, DIP 준수

  • ApplicationContext를 스프링 컨테이너라 한다.

    • 더 정확히는 스프링 컨테이너를 부를 때 BeanFactory, ApplicationContext로 구분해서 이야기한다.
  • ApplicationContext는 인터페이스다.

//스프링 컨테이너 생성
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(AppConfig.class);

Spring Bean

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

  • Bean은 new 키워드 대신 사용한다.
  • Spring Container가 제어한다.

Spring Bean의 특징

  1. Spring 컨테이너에 의해 생성되고 관리된다.
  2. 기본적으로 Singleton으로 설정된다.
  3. 의존성 주입(DI)을 통해 다른 객체들과 의존 관계를 맺을 수 있다.
  4. 생성, 초기화, 사용, 소멸의 생성주기를 가진다.

등록 방법

  • XML, Java Annotation, Java 설정파일 등을 통해 Bean 등록
  • Annotation
    • @ComponentScan
// 이 클래스를 Bean으로 등록
// @Controller, @Service, @Repository
@Component
public class MyService {
 	public void doSomething() {
 		System.out.println("Spring Bean 으로 동작");
 	}
}

IOC/DI

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

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

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

DI(의존성 주입, Dependency Injection)

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

  • new () ❌ → 생성하지 않는다.

정리

  1. IoC는 객체의 제어권을 개발자가 아닌 Spring Container 에 넘기는 개념, Spring이 객체 생성과 관리를 담당한다.

  2. DI는 Spring이 객체 간의 의존성을 자동으로 주입해주는 기법이다.

  3. 의존관계 주입은 객체 간의 결합도를 낮추고 코드의 유연성과 테스트 가능성을 높여준다.

Singleton Pattern

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

싱글톤 패턴의 등장

  • Web Application은 불특정 다수의 고객이 동시에 많은 요청을 보낸다.
  • 요청을 할 때 마다 객체가 새로 생성되고 처리가 완료되면 소멸된다.
  • 메모리 낭비가 아주 심하다.

싱글톤 패턴 적용

  • 객체가 한번만 생성되어 리소스를 절약할 수 있다.
  • Spring Bean은 싱글톤으로 관리되는 객체이다.

싱글톤 패턴의 주의점

  • 상태 유지(stateful)의 문제점
    • 데이터의 불일치나 동시성 문제가 발생할 수 있다.
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)로 설계를 해야한다.
    • 특정 클라이언트에 의존적인 필드가 있거나 변경할 수 있으면 안된다.

🤔 불특정 다수의 클라이언트(Thread)가 하나의 Controller(Bean)를 공유하는 것이 가능한가?

  • Controller 객체 하나를 생성하면 객체 자체는 Heap에 작성되지만, 해당 class 정보는 Method 영역에 저장된다. 즉, 모든 쓰레드에서 객체의 Binary Code 정보를 공유할 수 있다.
    → 공유되는 정보를 사용하기 위해여 Controller 를 사용하고 있는 쓰레드나 Controller 객체가 Block될 필요는 없다.

  • Controller가 내부적으로 상태를 갖는 것이 없으니, 메소드 호출만 하면 되기 때문에 굳이 동기화할 필요가 없다.

Spring Bean 등록

@ComponentScan

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

@SpringBootApplication

SpringBoot 프로젝트를 생성하면 main() 메서드가 있는 클래스 상단에 @SpringBootApplication 어노테이션이 존재한다.

  • ComponentScan의 동작순서
  1. Spring Application이 실행되면 @ComponentScan이 지정된 패키지를 탐색한다.
  2. 해당 패키지에서 @Component 또는 어노테이션이 붙은 클래스를 찾는다.
  3. 찾은 클래스를 Spring 컨테이너에 Bean 으로 등록한다.
  4. 등록된 Bean은 의존성 주입(DI)와 같은 방식으로 다른 Bean과 연결된다.

의존관계 주입

📚 의존관계 주입을 하는 방법으로 생성자 주입, setter 주입, 필드 주입, 메서드 주입 총 4가지 방법이 존재한다.

생성자 주입

  • 생성자를 통해 의존성을 주입하는 방법
  • 최초에 한번 생성된 후 값이 수정되지 못한다. (불변, 필수)
@Service
public class ScheduleServiceImpl implements ScheduleService {

    private final ScheduleRepository scheduleRepository;
    private final UserRepository userRepository;

    public ScheduleServiceImpl(ScheduleRepository scheduleRepository, UserRepository userRepository) {
        this.scheduleRepository = scheduleRepository;
        this.userRepository = userRepository;
    }
    // ...
}

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

  • 불변(immutable)
    • 어떤 요리(Web Application)를 만들지 정해졌다면 이미 재료(Bean)와 의존 관계가 결정된다.
    • 객체를 생성할 때 최초 한번만 호출된다.(불변)

@RequiredArgsConstructor

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

@Service
@RequiredArgsConstructor
public class ScheduleServiceImpl implements ScheduleService {

    private final ScheduleRepository scheduleRepository;
    private final UserRepository userRepository;
}
profile
안녕하세요 :)

0개의 댓글