Spring/강의

[📙 숙련 Spring] 1-1. 객체 지향과 Spring 핵심 개념

가지코딩 2025. 5. 15. 13:42

📙 목차

  1. SOLID 원칙
  2. Spring과 객체 지향 (IoC, DI) 
  3. Spring의 핵심 개념 (Spring Container, Spring Bean)
  4. 싱글톤(Singleton)

1. SOLID 원칙

SOLID 원칙

  • 객체 지향 설계에서 소프트웨어의 유지보수성, 확장성, 유연성을 높이기 위한 5가지 기본 설계 원칙

 

SOLID 원칙의 종류

  • 단일 책임 원칙 SRP(Single Responsibility Principle)
  • 개방 폐쇄 원칙 OCP(Open Closed Principle)
  • 리스코프 치환 원칙 LSP(Liskov Substitution Principle)
  • 인터페이스 분리 원칙 ISP(Interface Segregation Principle)
  • 의존관계 역전 원칙 DIP(Dependency Inversion Principle)
더보기

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

  • 하나의 클래스는 하나의 책임만 가져야 한다.
// 위반 예
public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // 사용자 정보를 저장하는 책임
    public void save() {
        System.out.println("저장 중: " + name);
    }

    // 이메일을 전송하는 책임
    public void sendEmail() {
        System.out.println("이메일을 보내는 중: " + email);
    }
}
// 적용 예
public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

// 사용자 정보를 DB에 저장하는 클래스
public class UserRepository {
    public void save(User user) {
        System.out.println("저장 중: " + user.getName());
    }
}

// 이메일 전송을 담당하는 클래스
public class EmailService {
    public void sendEmail(User user) {
        System.out.println("이메일을 보내는 중: " + user.getEmail());
    }
}

 

 

개방 폐쇄 원칙 OCP(Open Closed Principle)

  • 확장에는 열려있어야 하며, 변경(수정)에는 닫혀 있어야 한다.
// 위반 예
public class Shape {
    public String type;
}

public class AreaCalculator {
    public double calculate(Shape shape) {
        if (shape.type.equals("circle")) {
            return Math.PI * Math.pow(5, 2);
        } else if (shape.type.equals("square")) {
            return 5 * 5;
        }
        return 0;
    }
}
// 적용 예
public interface Shape {
    double calculateArea();
}

public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * Math.pow(radius, 2);
    }
}

public class Square implements Shape {
    private double side;

    public Square(double side) {
        this.side = side;
    }

    @Override
    public double calculateArea() {
        return side * side;
    }
}

public class AreaCalculator {
    public double calculate(Shape shape) {
        return shape.calculateArea();
    }
}

 

 

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

  • 하위타입을 상위타입으로 교체해도 기능에 문제가 없어야 한다
// 위반 예
public class Bird {
    public void fly() {
        System.out.println("The bird is flying");
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins cannot fly");
    }
}
// 적용 예
public abstract class Bird {
    public abstract void move();
}

public class Sparrow extends Bird {
    @Override
    public void move() {
        System.out.println("The sparrow is flying");
    }
}

public class Penguin extends Bird {
    @Override
    public void move() {
        System.out.println("The penguin is swimming");
    }
}

 

 

인터페이스 분리 원칙 ISP(Interface Segregation Principle)

  • 사용하지 않는 기능에 의존해서는 안된다
// 위반 예
public interface Employee {
    void work();
    void eat();
}

public class Manager implements Employee {
    @Override
    public void work() {
        System.out.println("Managing the team");
    }

    @Override
    public void eat() {
        System.out.println("Eating lunch");
    }
}

public class Intern implements Employee {
    @Override
    public void work() {
        System.out.println("Assisting with tasks");
    }

    @Override
    public void eat() {
        // 불필요한 메소드 구현
    }
}
// 적용 예
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class Manager implements Workable, Eatable {
    @Override
    public void work() {
        System.out.println("Managing the team");
    }

    @Override
    public void eat() {
        System.out.println("Eating lunch");
    }
}

public class Intern implements Workable {
    @Override
    public void work() {
        System.out.println("Assisting with tasks");
    }
}

 

 

의존관계 역전 원칙 DIP(Dependency Inversion Principle)

  • 세부사항에 의존하지 말고 추상화에 의존하라
// 위반 예
public class UserRepository {
    public void save(User user) {
        System.out.println("Saving user: " + user.getName());
    }
}

public class UserService {
    private UserRepository userRepository;

    public UserService() {
        this.userRepository = new UserRepository(); // 구체적인 클래스에 의존
    }

    public void registerUser(User user) {
        userRepository.save(user);
    }
}

public class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
// 적용 예
public interface UserRepository {
    void save(User user);
}

public class DatabaseUserRepository implements UserRepository {
    @Override
    public void save(User user) {
        System.out.println("Saving user to the database: " + user.getName());
    }
}

public class FileUserRepository implements UserRepository {
    @Override
    public void save(User user) {
        System.out.println("Saving user to a file: " + user.getName());
    }
}

public class UserService {
    private UserRepository userRepository;

    // 생성자 주입을 통해 의존성 주입
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void registerUser(User user) {
        userRepository.save(user);
    }
}

public class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

2. Spring과 객체 지향

Spring은 OCP(개방-폐쇄 원칙)DIP(의존 관계 역전 원칙)처럼 다형성만으로는 구현하기 어려운 객체 지향 설계 원칙들을 IOC(제어의 역전)DI(의존성 주입)를 통해 보다 효율적으로 구현할 수 있도록 지원한다.

 

 

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

  • 제어의 흐름을 반전시킨다는 뜻
  • 객체를 생성하고 관리하는 책임을 스프링 컨테이너에 맡기는 방식
// 전통적인 방식
Car car = new Car();

// Spring에서는 이 객체 생성과 초기화 과정을 스프링 컨테이너가 관리한다.
Car car = context.getBean(Car.class);

 

 

DI (Dependency Injection, 의존성 주입)

  • 객체 간의 의존 관계를 주입하는 방식
  • 객체가 필요한 다른 객체를 스스로 생성하거나 관리하지 않고, 외부에서 주입하는 방식으로 의존성을 해결하는 방식
// 전통적인 방식
public class Car {
    private Engine engine = new Engine(); // 직접 Engine 객체를 생성
}

// 외부에서 Engine 객체를 주입
public class Car {
    private final Engine engine;

    // DI를 통해 외부에서 Engine을 주입
    @Autowired
    public Car(Engine engine) {
        this.engine = engine;
    }
}

 

 

IoC와 DI 의 장점

  • 유연성 향상: 객체의 생성과 의존 관계를 스프링이 관리하므로, 직접 객체를 관리하지 않고도 필요한 객체를 쉽게 교체할 수 있다.
  • 테스트 용이성: 의존성을 외부에서 주입받기 때문에, 테스트할 때도 실제 구현체가 아닌 가짜 객체(mock object)를 주입하여 테스트할 수 있다.
  • 결합도 감소: 의존성 주입을 통해 클래스들 간의 결합도가 낮아진다. 즉 클래스들이 서로 직접적으로 의존하지 않고, 외부에서 주입받으므로 코드 변경이 쉬워진다.

3. Spring의 핵심 개념

Spring Container

  • Spring 애플리케이션에서 객체들을 관리하는 역할을 한다.
  • 객체를 생성, 관리, 소멸하는 작업을 담당하며, 애플리케이션이 시작될 때 설정 파일이나 어노테이션을 통해 필요한 객체(Bean)를 생성하고 주입한다.
  • 역할: 객체 생성, 의존성 주입, 객체 생명주기 관리
  • 종류
    • BeanFactory: Spring Container의 기본 인터페이스로, 객체를 생성하고 관리한다.
    • ApplicationContext: BeanFactory의 확장판으로, 국제화, 환경변수 관리 등 추가적인 기능을 제공한다. ApplicationContext를 주로 사용한다.

 

Spring Bean

  • Spring Bean은 Spring Container가 관리하는 객체
  • 특징
    • 기본적으로 싱글톤(Singleton)으로 설정되어 동일한 객체 인스턴스를 재사용한다.
    • DI를 통해 다른 객체에 의존성을 주입받는다.

4. 싱글톤(Singleton)

싱글톤(Singleton)

  • 애플리케이션 실행 중 하나의 인스턴스만 생성되어 재사용되는 객체를 의미한다.
  • 같은 객체를 여러 번 만들지 않고 한 번만 만들어서 재사용하는 디자인 패턴

 

싱글톤 특징

  • 객체 1개만 존재: 클래스가 최초 요청될 때 단 1개의 인스턴스를 생성하고, 그 이후에는 같은 인스턴스를 계속 반환한다.
  • 공유 인스턴스: 여러 곳에서 동일한 객체를 참조하게 되므로, 공통된 설정이나 자원을 관리할 때 유용하다.
  • 메모리 효율성 증가: 객체를 반복해서 생성하지 않으므로 메모리 낭비가 줄어든다.
  • 스프링의 기본 Bean 설정: 스프링 컨테이너에 등록된 Bean은 기본적으로 모두 싱글톤이다.

 

싱글톤 패턴의 주의점

  • 상태 유지(stateful)의 문제점: 데이터의 불일치나 동시성 문제가 발생할 수 있다.