단일 책임 원칙 SRP (Single Responsibility Principle)
개방 폐쇄 원칙 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 메서드)
👉 객체의 생성, 사용 등을 자동으로 설정해주는 ###가 필요하다.
리스코프 치환 원칙 LSP (Liskov Substitution Principle)
인터페이스 분리 원칙 ISP (Interface Segregation Principle)
의존관계 역전 원칙 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은 다형성 만으로는 해결하지 못했던 객체 지향 설계 OCP, DIP를 IOC, DI를 통해 가능하도록 만들어준다.
💡 실무에서는 추상화 과정에서 비용(시간)이 발생하기 때문에 기능을 확장할 가능성이 없다면 구현 클래스를 직접 사용하고 추후 변경된다면 인터페이스로 리팩토링 한다.
📚 Spring으로 구성된 애플리케이션에서 객체(Bean)을 생성, 관리, 소멸하는 역할을 담당한다. 애플리케이션 시작 시, 설정 파일이나 Annotation을 읽어 Bean을 생성하고 주입하는 모든 과정을 컨트롤한다.

ApplicationContext를 스프링 컨테이너라 한다.
ApplicationContext는 인터페이스다.
//스프링 컨테이너 생성
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(AppConfig.class);
Spring 컨테이너가 관리하는 객체를 의미한다. 자바 객체 자체는 특별하지 않지만, Spring이 객체를 관리하는 순간부터 Bean이 된다. Spring은 Bean을 생성, 초기화, 의존성 주입 등을 통해 관리한다.

@ComponentScan// 이 클래스를 Bean으로 등록
// @Controller, @Service, @Repository
@Component
public class MyService {
public void doSomething() {
System.out.println("Spring Bean 으로 동작");
}
}
객체 생성과 관리 권한을 개발자가 아닌 Spring 컨테이너가 담당하는 것을 말한다. 기본적으로 개발자가 객체를 직접 생성하고 관리했지만, Spring 에서는 컨테이너가 객체 생성, 주입, 소멸을 관리한다.
Spring이 객체 간의 의존성을 자동으로 주입해주는 것을 의미한다. 한 객체가 다른 객체를 사용할 때, 해당 객체를 직접 생성하지 않고 Spring이 주입해주는 방식이다. IOC를 구현하는 방식 중 하나이다.
IoC는 객체의 제어권을 개발자가 아닌 Spring Container 에 넘기는 개념, Spring이 객체 생성과 관리를 담당한다.
DI는 Spring이 객체 간의 의존성을 자동으로 주입해주는 기법이다.
의존관계 주입은 객체 간의 결합도를 낮추고 코드의 유연성과 테스트 가능성을 높여준다.
📚 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴


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 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.🤔 불특정 다수의 클라이언트(Thread)가 하나의 Controller(Bean)를 공유하는 것이 가능한가?
Controller 객체 하나를 생성하면 객체 자체는 Heap에 작성되지만, 해당 class 정보는 Method 영역에 저장된다. 즉, 모든 쓰레드에서 객체의 Binary Code 정보를 공유할 수 있다.
→ 공유되는 정보를 사용하기 위해여 Controller 를 사용하고 있는 쓰레드나 Controller 객체가 Block될 필요는 없다.
Controller가 내부적으로 상태를 갖는 것이 없으니, 메소드 호출만 하면 되기 때문에 굳이 동기화할 필요가 없다.
📚 Spring이 특정 패키지 내에서 @Component , @Service , @Repository , @Controller 같은
Annotation이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록하는 기능이다. 개발자가
Bean을 직접 등록하지 않고도 Spring이 자동으로 관리할 객체들을 찾는다.
SpringBoot 프로젝트를 생성하면 main() 메서드가 있는 클래스 상단에 @SpringBootApplication 어노테이션이 존재한다.

@ComponentScan이 지정된 패키지를 탐색한다.@Component 또는 어노테이션이 붙은 클래스를 찾는다.📚 의존관계 주입을 하는 방법으로 생성자 주입, 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;
}
// ...
}
📚 실제 Web Application을 개발하면 대부분이 불변 객체이고 생성자 주입 방식을 선택하게 된다. 이런 반복되는 코드를 편안하게 작성하기 위해 Lombok에서 제공하는 Annotation 이다.
@Service
@RequiredArgsConstructor
public class ScheduleServiceImpl implements ScheduleService {
private final ScheduleRepository scheduleRepository;
private final UserRepository userRepository;
}