SPRING 프레임워크를 통해 객체지향 설계의 원칙 중 SOLID 원칙이 무엇이며, 어떻게 적용되는지 알아보겠습니다.
1. SRP (Single Responsibility Principle) - 단일 책임 원칙
각 클래스는 하나의 단일한 책임만 가져야 한다.
Spring에서는 Controller, Service, Repository 등으로 역할을 명확히 나누어 각각의 클래스가 특정 기능 또는 관심사에만 집중하도록 합니다.
SRP를 잘 준수했는지 알 수 있는 기준은 변경입니다.
변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따랐다고 할 수 있습니다.
// UserController 클래스는 사용자 관리와 관련된 HTTP 요청을 처리하는 책임만을 가져야 합니다.
@Controller
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
// 사용자 생성 요청 처리
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.ok(createdUser);
}
// 다른 사용자 관리 관련 메서드들...
}
2. OCP (Open / Closed Principle) - 개방 폐쇄 원칙
클래스는 확장에는 열려있고 수정에는 닫혀 있어야 합니다.
Spring에서는 이를 위해 다형성(Polymorphism)과 의존성 주입(Dependency Injection:DI)을 활용합니다.
새로운 기능을 추가할 때 기존 코드를 변경하지 않고 간편하게 확장할 수 있습니다.
어떻게 간편하게 확장하는지?
서비스에 새로운 인증 기능을 추가해야 할 때,
새로운 AuthenticationService 인터페이스를 구현하고 이를 사용하여 새로운 인증 방식을 추가할 수 있습니다.
기존 코드를 수정할 필요 없이 새로운 구현체를 생성하여 연결합니다.
즉, 인터페이스를 통해 새로운 구현체를 생성하여 확장에 열려있어야 함을 의미합니다.
문제점!!!
클라이언트(개발자)가 직접 AuthenticationService을 구현한 Class를 선택합니다.
구현 객체를 변경하려면 클라이언트 코드를 직접 변경해야 합니다.
위 문제점을 통해 다형성을 지켰지만, OCP 원칙은 지키지 못하게 됩니다.
하지만, Spring을 통해 개발을 하면 스프링 빈 등록과 Spring Container를 통해 위 문제점을 보완하며, OCP 원칙을 지킬 수 있습니다.
// UserRepository 인터페이스를 구현한 MySQLUserRepository 클래스
@Repository
public class MySQLUserRepository implements UserRepository {
// MySQL 데이터베이스와 상호 작용하는 메서드들...
}
// UserRepository 인터페이스를 구현한 PostgreSQLUserRepository 클래스
@Repository
public class PostgreSQLUserRepository implements UserRepository {
// PostgreSQL 데이터베이스와 상호 작용하는 메서드들...
}
3. LSP (Liskov Substituion Principle) - 리스코프 치환 원칙
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서, 하위 타입의 인스턴스로 변경할 수 있어야 합니다.
즉, 하위 타입은 언제나 상위 타입으로 교체할 수 있어야 합니다.
인터페이스에 대한 구현체가 있을 때,
ex) 자동차 인터페이스를 구현한 아반떼에 악셀 기능이 앞으로 갈 수 있고, 뒤로도 갈 수 있게 할 수 있습니다.
아반떼 구현체에 이렇게 구현해도 문제는 없으나, 악셀을 밟으면 앞으로 간다가 규약이라면 이 규약을 무조건 지켜야 된다는 것이 LSP 원칙입니다.
Spring에서는 인터페이스를 통해 LSP를 지원합니다.
// UserRepository 인터페이스를 구현한 MySQLUserRepository 클래스
@Repository
public class MySQLUserRepository implements UserRepository {
// MySQL 데이터베이스와 상호 작용하는 메서드들...
}
// UserRepository 인터페이스를 구현한 PostgreSQLUserRepository 클래스
@Repository
public class PostgreSQLUserRepository implements UserRepository {
// PostgreSQL 데이터베이스와 상호 작용하는 메서드들...
}
// UserRepository 타입으로 사용 가능한 메서드
public void someMethod(UserRepository userRepository) {
// UserRepository를 사용하는 로직...
}
4. ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
Spring에서는 이를 인터페이스를 작게 분리하여 해결합니다.
작게 분리한다는 것은, 클라이언트가 필요로 하는 기능에만 해당하는 작은 인터페이스를 정의하여 사용하는 것을 의미합니다.
예를 들어, UserService 인터페이스는 createUser, updateUser와 같은 사용자 관리와 관련된 메서드만을 포함하고 있어야 합니다. 대신, createUser, updateUser 메서드는 사용자 관리에 필요한 서비스에 의존하고, 다른 기능은 사용하지 않습니다.
// UserService 인터페이스
public interface UserService {
User createUser(User user);
User updateUser(User user);
// 다른 사용자 관리 관련 메서드들...
}
// UserManagementService 인터페이스 (ISP 적용)
public interface UserManagementService {
User createUser(User user);
User updateUser(User user);
// createUser, updateUser 메서드만을 포함하는 작은 인터페이스
}
5. DIP (Dependency Inversion Principle) - 의존성 역전 원칙
추상화에 의존해야 하며, 구체화에는 의존하지 않아야 합니다.
Spring에서는 의존성 주입을 통해 이를 지원합니다.
즉, 클래스는 직접적으로 의존하는 객체를 생성하지 않고 대신에 외부 (스프링부트)에서 의존 객체를 주입받도록 합니다.
예를 들어, UserService 클래스가 UserRepository에 의존해야 할 때, 이를 생성자 주입(Constructor Injection)을 통해 UserRepository 객체를 받아와야 합니다. 이렇게 하면 UserService 클래스는 UserRepository의 구현 세부 사항에 직접적으로 의존하지 않고, 언제든지 다른 UserRepository 구현체로 교체할 수 있습니다.
// UserService 클래스가 UserRepository에 의존하는 예시 (의존성 주입을 통해 DIP를 준수)
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
Spring 에서 SOLID를 지원해 주는 기능들
IOC (Inversion of Control)
기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행합니다.
하지만 스프링 부트를 사용하게 되면 개발자는 구현 객체는 자신의 로직만 실행하는 역할을 담당합니다.
프로그램의 제어 흐름은 스프링 컨테이너가 담당하여, 프로그램의 제어 흐름을 외부에서 관리해 주어 이를 IOC라 부릅니다.
DI (Dependency Injection)
애플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성하고, 클라이언트에 전달해서 클라이언트 서버의 실제 의존관계가 연결되는 것을 의존성 주입 (DI)라 합니다.
한 객체가 다른 객체를 생성하거나 사용하는 것이 아니라,
- 필요한 객체를 외부에서 주입받는 디자인 패턴입니다. 이를 통해 객체 간의 결합도를 낮추고 유연하고 재사용 가능한 코드를 작성할 수 있습니다.
일반적으로, 한 클래스가 다른 클래스의 인스턴스를 생성하거나 그 클래스에 직접적으로 의존할 때, 이를 의존성이 강하다고 합니다. 이는 코드를 유연하게 만들기 어렵게 만들 수 있습니다. 왜냐하면 만약 의존하는 클래스가 변경되면 그 클래스를 사용하는 모든 곳에서 변경이 필요하기 때문입니다.
의존성 주입은 이러한 문제를 해결하기 위해 사용됩니다.
클래스는 필요한 의존성을 외부에서 주입받습니다. 이것은 주로 생성자(Constructor), Setter 메서드, 또는 인터페이스를 통해 이루어집니다.
이렇게 하면 클래스는 외부에 종속되어 있지 않으며, 의존성이 변경되어도 해당 클래스를 수정할 필요가 없어집니다.
public interface Service {
void performAction();
}
public class SomeService implements Service {
public void performAction() {
// 구체적인 동작 구현
}
}
public class Client {
private Service service;
public Client(Service service) {
this.service = service;
}
public void doSomething() {
service.performAction();
}
}
이렇게 하면 Client 클래스는 Service 인터페이스에 의존하게 되고, 특정 구현체에 의존하지 않습니다.
대신에 클라이언트 코드가 호출하는 대상의 구체적인 구현체는 클라이언트 외부에서 주입될 수 있습니다.
이것이 "클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다"는 의미입니다.
'Spring Framework > Spring' 카테고리의 다른 글
[Spring Cloud] Eureka Server, Discovery Service 이해하기 (0) | 2024.03.31 |
---|---|
[Spring] Spring CORS 설정 & 이슈 해결 및 웹 애플리케이션 통신 이해하기 (0) | 2024.02.24 |
[Spring] 스프링 파일, 이미지 업로드 / 인스타 이미지 업로드 및 해시태그 파싱하기 - MultipartFile, File (0) | 2023.12.21 |
[Spring] 스프링 중복 빈 해결 방법, Bean이 2개 이상일 때 (0) | 2023.04.01 |
[Spring] 스프링 싱글톤 컨테이너, 스프링 컨테이너 (0) | 2023.03.28 |