SOLID 원칙
SOLID: 시간이 지나도 유지보수, 확장이 쉬운 소프트웨어를 만들기 위한 원칙( 객체 지향 원칙)
Fast PreView
SRP (단일 책임 원칙): 클래스당 1개의 책임
OCP ( 개방-폐쇄 원칙): 새로운 기능이 추가되거나 변경이 발생할 때 기존 코드를 수정하지 않고 확장할 수 있어야 합니다. 이를 위해 추상화, 다형성, 인터페이스 등을 활용하여 모듈 간의 의존성을 최소화하고 변경에 유연하게 대처할 수 있습니다.
LSP (리스코프 치환 원칙): 자식 클래스는 언제나 부모 클래스의 자리에 사용될 수 있어야 합니다.
ex) discountPolicy 인터페이스 -> 해당 인터페이스를 implements하는 fixedDiscountPolicy, percentDisCountPolicy 로 타입 캐스팅 가능하다.
ISP (Interface Segregation Principle, 인터페이스 분리 원칙): 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 합니다. (인터페이스를 잘게 쪼게서, 각 구현체가 필요한 메소드만 딱딱 가져와서 쓰게)
DIP (Dependency Inversion Principle, 의존성 역전 원칙): 추상화(인터페이스, 추상 클래스)에 의존해야 하며, 구체화(구현 클래스)에 의존해서는 안 됩니다. 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것에 의존하기 보다는, 변화하기 어려운것, 거의 변화가 없는 것에 의존하라는 원칙
또한, 의존성 주입(Dependency Injection)을 통해 모듈 간의 의존 관계를 런타임에 설정할 수 있습니다.
의존성 주입을 통해 추상화를 사용하고, 구체적인 구현에는 의존하지 않아야 합니다.
요구사항이 정해지면 상세수준(저수준모듈, 구체적인 클래스)의 변경이 발생할 가능성이 높아진다.
하지만 상위수준(고수준모듈, 인터페이스 혹은 추상클래스)은 한 번 안정화되면 쉽게 변하지 않기 때문이다
이를 통해 abstraction(추상화)에 대한 기반을 제공한다. 따라서, LSP와 함께 OCP를 따르는 설계를 만들어주는 기반이다
+) OCP는 추상화와 다형성으로 구현하는데, 각각은 DIP와 LSP이 기반이 됨
IoC 란?
Inversion of Control 의 줄임말로, 제어의 역전 이라는 뜻이 된다. 제어의 역전이란 메소드나 객체의 호출작업을 개발자가 아닌 외부에서 결정되는 것을 의미한다.
필요한 객체들은 Ioc컨테이너로 부터 DI받아서 사용하는데, 이 객체들은 Ioc 컨테이너가(AppConfig)
관리하기에 객체는 자신의 로직에만 집중 가능하다.
예를 들면 자바 프로그램은 main() 메소드에서 시작하여 개발자가 미리 정한 순서를 따라 객체가 생성되고 실행된다. 그런데 서블릿을 생각해보면 개발해서 서버로 배포할 수 있지만, 배포하고 나서는 개발자가 직접 제어할 수 없고 서블릿에 대한 제어 권한을 가진 컨테이너가 적절한 시점에 서블릿 클래스의 객체를 만들고 그 안에 메소드를 호출한다. 대부분의 프레임 워크는 이와 같은 방식으로 사용 되며, 개발자는 필요한 부분을 개발해서 끼워넣기 형식으로 개발하고 실행한다. 이와 같이 조립된 코드의 최종 호출은 개발자에 의해 제어되는 것이 아닌 프레임워크에 의해 제어 되게 되는데 이 때문에 제어의 역전이라 하는 것 이다.
DI 란?
Dependency Injection 의 줄임말로, 의존성 주입 이라는 뜻이 된다. 의존성 주입은 제어의 역전이 일어날때 스프링 내부에 있는 객체들간의 관계를 관리할 때 사용하는 기법이다. 자바에서는 일반적으로 인터페이스를 이용해서 의존적인 객체의 관계를 최대한 유연하게 처리할 수 있도록 한다.
의존성 주입은 의존적인 객체를 직접 생성하거나 제어하는것이 아닌, 특정 객체에 필요한 객체를 외부에서 결정해서 연결 시키는 것을 의미한다. 우리는 클래스의 기능을 추상적으로 묶어둔 인터페이스를 갖다 쓰면 되는 것 이다. 이러한 의존성 주입으로인해 모듈간의 결합도가 낮아지고 유연성이 높아진다.
스프링 프레임워크가 SOLID 관점에서 미치는 영향
스프링 프레임워크는 SOLID 원칙을 지원하고 이를 통해 객체지향 설계의 품질을 향상시킵니다. 아래에서는 스프링 프레임워크가 SOLID 원칙을 지원함으로써 얻을 수 있는 이점들을 정리해드리겠습니다:
SRP (Single Responsibility Principle):
스프링 프레임워크는 IoC (Inversion of Control) 컨테이너를 통해 객체의 생성과 의존성 주입을 담당합니다. 이를 통해 객체의 단일 책임인 생성과 의존성 관리를 분리할 수 있습니다. 이는 코드의 가독성과 유지보수성을 향상시키는데 도움을 줍니다.
OCP (Open-Closed Principle):
스프링 프레임워크는 인터페이스와 추상화를 적극적으로 활용하여 느슨한 결합을 지원합니다. 이를 통해 새로운 기능을 추가하거나 변경할 때 기존 코드를 수정하지 않고도 확장이 가능합니다. 스프링의 다양한 확장 포인트(인터셉터, 어드바이스, 리스너 등)를 이용하여 애플리케이션의 동작을 변경하거나 확장할 수 있습니다.
LSP (Liskov Substitution Principle):
스프링은 인터페이스를 중심으로 다형성을 지원하며, 클라이언트는 인터페이스에 의존합니다. 이를 통해 상속 관계에 있는 클래스들을 대체 가능하도록 합니다. 스프링의 DI (Dependency Injection) 기능을 사용하면 클라이언트는 구체적인 구현체 대신 인터페이스를 주입받으므로, 새로운 구현체를 추가하거나 변경할 때 기존 코드를 수정할 필요가 없습니다.
DIP (Dependency Inversion Principle):
스프링 프레임워크는 의존성 주입(DI)을 통해 DIP를 지원합니다. 객체 간의 의존성을 역전시키고, 인터페이스나 추상화에 의존하도록 설계합니다. 이를 통해 객체 간의 결합도를 낮추고, 코드의 재사용성과 유연성을 향상시킵니다. 스프링의 의존성 주입은 객체를 직접 생성하거나 의존 객체를 결정하는 책임을 갖지 않고, [외부에서 의존 객체를 주입받아 사용(의존성 역전)]합니다. 이는 객체 간의 결합도를 줄여 변경에 민감하지 않은 코드를 작성할 수 있도록 도와줍니다.
느슨한 결합 (Loose Coupling):
스프링은 인터페이스와 추상화를 통해 객체 간의 느슨한 결합을 지원합니다. 각 구성 요소는 자신이 필요로 하는 인터페이스에만 의존하므로, 구체적인 구현체에 대한 의존성을 낮출 수 있습니다. 이는 코드의 유연성과 확장성을 높여줍니다. 또한, 스프링의 IoC 컨테이너를 통해 객체의 생명주기와 의존성 관리를 일관되게 처리함으로써 결합도를 낮춥니다.
코드의 재사용성과 유지보수성:
스프링은 SOLID 원칙을 따르고 객체지향 설계의 품질을 향상시키는 다양한 기능과 구성 요소를 제공합니다. 이를 통해 코드의 재사용성과 유지보수성을 향상시킬 수 있습니다. 스프링의 다양한 모듈과 기능을 이용하여 개발하면, 새로운 기능을 추가하거나 변경할 때 기존 코드를 수정하는 대신 구성 요소를 조립하거나 확장할 수 있습니다. 이는 변경에 유연하고 확장 가능한 애플리케이션을 개발하는데 도움을 줍니다.
요약
스프링 프레임워크는 SOLID 원칙을 준수하고 이를 지원하는 다양한 기능과 구성 요소를 제공하여 객체지향 설계의 품질을 향상시킵니다. 이는 코드의 가독성, 유지보수성, 재사용성, 확장성 등을 개선하여 개발자가 효율적으로 안정적이고 유연한 애플리케이션을 개발할 수 있도록 도와줍니다.
스프링 프레임워크를 사용한 프로젝트에서 SOLID를 준수한 코드 예시
SRP, ISP -> 객체가 커지지 않도록 막아준다. -> 변경을 최소화한다.
SRP: 1클래스 1책임
ISP: 인터페이스를 잘게 분리하는 것
ISP 예시
public interface PaymentProvider {
void processPayment(double amount);
void refundPayment(double amount);
void verifyPayment(double amount);
void cancelPayment(double amount);
}
위와같이 "결제" 관련 로직을 하나의 인터페이스에 몰아 넣을 경우, 어떤 문제가 생길까?
// 은행에서의 payment
public class BankPaymentProvider implements PaymentProvider {
@Override
public void processPayment(double amount) {
// 은행 결제 서비스 처리 로직
}
@Override
public void refundPayment(double amount) {
// 은행 결제 환불 처리 로직
}
}
// 모바일에서의 payment
public class MobilePaymentProvider implements PaymentProvider {
@Override
public void verifyPayment(double amount) {
// 모바일 결제 검증 처리 로직
}
@Override
public void cancelPayment(double amount) {
// 모바일 결제 취소 처리 로직
}
}
위와 같이 각각의 구현체가 모든 메소드를 필요로하지 않을 수 있다.
하지만 위의 경우, 각 구현체가 자신의 사용하지 않은 메소드까지 의존해야한다. ( 인터페이스에 선언되어 있기에)
따라서 인터페이스를 아래와 같이 분리해야한다.
public interface PaymentProcessor {
void processPayment(double amount);
void refundPayment(double amount);
}
public interface PaymentVerifier {
void verifyPayment(double amount);
}
public interface PaymentCanceler {
void cancelPayment(double amount);
}
Lsp
ex) memberService -> memberServiceImpl
ex) discountPolicy 인터페이스 -> 해당 인터페이스를 implements하는 fixedDiscountPolicy, percentDisCountPolicy 로 타입 캐스팅 가능하다.
다형성 예시
DiscountPolicy discountPolicy=new fixedDiscountPolicy(~~);//이제 fixedDiscountPolicy구현체 적용
DiscountPolicy discountPolicy=new percentDiscountPolicy(~~);//percentDiscountPolicy구현체 적용
즉, 리스코프 치환법칙은 OCP를 받쳐주는 다형성에 대한 원칙을 제공한다.
OCP 예시
public interface Book {
void display();
}
위와 같은 Book 인터페이스를 아래의 3가지 클래스가 상속받는다.
public class PaperBook implements Book {
private String title;
private String author;
// PaperBook에 대한 구현 내용
@Override
public void display() {
System.out.println("PaperBook: " + title + " by " + author);
}
}
public class EBook implements Book {
private String title;
private String author;
// EBook에 대한 구현 내용
@Override
public void display() {
System.out.println("EBook: " + title + " by " + author);
}
}
}
이제 Spring data JPA를 사용해서 BookRepository를 정의 한다.
import org.springframework.data.repository.CrudRepository;
public interface BookRepository extends CrudRepository<Book, Long> {
// 기타 필요한 메서드들
}
이제 서비스로직을 구현한 Library 클래스에서, BookRepsitory를 DI 받아서 사용한다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@RequiredArgConstructor
public class Library {
private BookRepository bookRepository;
public void addBook(Book book) {
bookRepository.save(book);
}
public void removeBook(Book book) {
bookRepository.delete(book);
}
public void displayBooks() {
Iterable<Book> books = bookRepository.findAll();
for (Book book : books) {
book.display();
}
}
}
위 예시에서, Library (서비스 계층) 은 addBook, removeBook 같은 CRUD 메소드를 사용하는데,
만약 새로운 종류의 책 (ex: 오디오북) 이 나오더라도(확장에 열려있다), Service 계층을 변경할 필요없이 (변경에 닫혀있고)
새로운 구현클래스를 추가하는 것으로 해결가능하다.
ex
public class AudioBook implements Book {
private String title;
private String author;
// AudioBook에 대한 구현 내용
@Override
public void display() {
System.out.println("AudioBook: " + title + " by " + author);
}
}
DIP 예시
Repository 인터페이스 선언
public interface UserRepository {
User findById(Long id);
User findByUsername(String username);
void save(User user);
void delete(User user);
// 기타 필요한 메서드들
}
Repository 구현 클래스
@Repository
public class JpaUserRepository implements UserRepository {
private final EntityManager entityManager;
@Autowired
public JpaUserRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public User findById(Long id) {
// JPA를 사용한 findById 구현
}
@Override
public User findByUsername(String username) {
// JPA를 사용한 findByUsername 구현
}
@Override
public void save(User user) {
// JPA를 사용한 save 구현
}
@Override
public void delete(User user) {
// JPA를 사용한 delete 구현
}
}
Service 영역에서의 사용
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
.....
// 기타 비즈니스 로직들
}
위와 같이, 스프링의 DI 를 사용하면 런타임에서 어떤 객체를 주입받을지 정할 수 있다.
Service 계층은 변경가능성이 낮은 인터페이스 계층에 의존하고, 구현 클래스에는 의존하지 않기에,
실제 repsitory 기능을 수행하는 JpaUserRepository가 변경되어도 변경되지 않을 수 있다.( OCP 에 도움)
위의 코드의 경우, @Service 를 통해 UserService를 스프링 빈으로 등록하여 IoC 컨테이너가 관리하도록 했기에,
JpaOrderRepository 빈을 주입하도록 생성자 주입을 수행하는 것으로, 스프링이 알아서 의존성을 해결해준다.
'Spring boot' 카테고리의 다른 글
[spring 데이터 접근 핵심 원리 6] 자바 예외의 이해 (0) | 2023.05.30 |
---|---|
[spring 데이터 접근 핵심 원리 5] transaction - 2 (트랜잭션 동기화) (0) | 2023.05.26 |
[spring 데이터 접근 핵심 원리 4] transaction - 1 (0) | 2023.05.25 |
[spring 데이터 접근 핵심 원리 3] 커넥션 풀, 데이터 소스 (connection pool, data source) (0) | 2023.05.25 |
[spring 데이터 접근 핵심 원리 2] jdbc를 통한 crud (0) | 2023.05.25 |