객체 지향 설계와 스프링
SOLID - 객체 지향 설계의 5가지 원칙
- SRP - 단일 책임 원칙(Single Responsibility Principle)
- 단일 책임 원칙은 “하나의 클래스는 하나의 책임만 가져야 한다”는 규칙이다.
- 그런데 “하나의 책임”이라는 것은 모모하다.
- 따라서 여러 기준이 있지만, 중요한 기준은 “변경”이다. 즉, 비즈니스 로직에 변경이 있을 때 파급효과가 적으면 단일 책임 원칙을 잘 따른 것이라고 할 수 있다.
- OCP - 개방-폐쇄 원칙(Open/Closed Principle)
- 개방-폐쇄 원칙은 다형성을 활용해 확장에는 열려 있지만, 변경에는 닫혀 있어야 한다.
- 스프링에서는 DI와 다형성을 활용해 해당 원칙을 만족시킬 수 있다.
- LSP - 리스코프 치환 원칙(Liskov Substitution Principle)
- 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- 즉, 다형성에서 하위 클래스는 인터페이스 규악을 모두 지켜야 한다는 뜻이다.
- ISP - 인터페이스 분리 원칙(Interface Segregation Principle)
- 인터페이스 분리 원칙은 특정 기능 구현을 위한 하나의 인터페이스보다 여러 개의 인터페이스로 분리하는 것이 낫다는 원칙이다.
- “인터페이스 분리 원칙”을 따른다면 “단일 책임 원칙”도 부수적으로 따라온다고 생각한다.
- DIP - 의존관계 역전 원칙(Dependency Inversion Principle)
- 의존관계 역전 원칙은 객체는 저수준 모듈보다 고수준 모듈에 의존해야 한다는 원칙이다.
- 쉽게 이야기하자면 구현 클래스에 의존하지 말고, 인터페이스에 의존한다는 뜻이다.
- “추상화에 의존하자.”
객체지향과 스프링의 관계에 대해 말하기 전에 좋은 객체 지향 설계를 위한 SOLID부터 언급한 이유는 결국 자바와 스프링이 SOLID를 활용해 확장 가능하고 유연한 설계가 가능하게끔 설계했기 때문이다.
객체 지향의 특징
객체 지향은 다음과 같은 특징을 갖는다.
- 추상화
- 캡슐화
- 상속
- 다형성
다음의 특징 중 다형성에 집중하려고 한다.
스프링은 DI와 DI 컨테이너 기술을 통해 객체 지향에서의 다형성을 통한 애플리케이션 설계를 가능토록 해준다.
DI 컨테이너는 DI(의존성 주입)을 위한 객체들을 싱글톤(기본)으로 관리하는데 해당 컨테이너에 등록된 객체들을 주입해준다.
DI 컨테이너 예제
사실 처음 DI와 DI Contaioner에 대한 개념을 이론적으로 학습했을 당시에는 제대로 와닿지가 않았는데, 프로젝트를 진행해보면서 DI와 DI 컨테이너 장점을 느낄 수 있었다.
프로젝트 생성
- 프로젝트에서는 간단한 예제를 위해 초기에 mysql로 repository를 구성했지만,
- 추후에 mssql로의 전환이 불가피해지는 상황에서 DI를 활용하지 않으면 발생하는 문제와 이를 DI를 활용해 어떻게 해결하는 지 알아보려 한다.
주의: DI와 DI 컨테이너를 위한 예제이기 떄문에 로직에 많은 문제가 있습나다.
// MemberRepositoryMysql.class
@Repository
public class MemberRepositoryMysql {
public Member save(Member member) {
return member;
}
}
// MemberService.class
@Service
public class MemberService {
private final MemberRepositoryMysql memberRepository;
public MemberService(MemberRepositoryMysql memberRepository) {
this.memberRepository = memberRepository;
}
Member save(Member member) {
return memberRepository.save(member);
}
}
// Member
@Data
public class Member {
String userId;
String userPassword;
public Member(String userId, String userPassword) {
this.userId = userId;
this.userPassword = userPassword;
}
}
-
차례대로 보면 MemberRepositoryMysql은 MySQL를 사용한다는 전제로 만들어진 Repository입니다. 눈여겨 볼 만한 부분은 @Repository Anntation입니다. @Repository는 Spring에서 제공하는 애노테이션으로 해당 프로젝트가 실행되면 @Repository를 갖는 클래스들을 모두 빈(default : singleton)으로 등록한다.
-
MemberService는 MysqlrepositoryMysql를 참조하고 있으며 @Service또한 Spring Frameword가 빈으로 등록해준다.
그런데 만약 MemberRepositoryMysql 클래스를 단지 하나의 클래스가 아닌 수십개 혹은 수백개의 클래스에서 MemberService와 같이 참조하고 있는 상황에서 MsSQL로의 데이터베이스 전환이 필요한 경우를 가정해보면 수백개의 클래스 모두 일일이 의존하고 있는 모든 코드를 변경해줘야 하는 상황이 발생할 수 있습니다. 그렇기 때문에 위의 코드는 스프링이 제공해주는 기능을 활용해서 확장이 용이한 코드로 변경시켜야 합니다.
번외로 @Repository, @Service 애노테이션을 살펴보면
@Retention(RetentionPolicy.RUNTIME)
인 것을 볼 수 있습니다. 즉, 런타임 시점까지 해당 애노테이션들이 유효하다는 것이고, 그 뜻은 Spring이 런타임 시점에 reflection을 활용해서 bean으로 등록하고 또 다른 작업을 수행할 것이라고 예상할 수 있습니다.
// MemberInterface.class
public interface MemberRepository {
Member save(Member member);
}
// MemberRepositoryMysqlImpl.class
public class MemberRepositoryMysqlImpl implements MemberRepository{
public Member save(Member member) {
return member;
}
}
// MemberService.class
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
Member save(Member member) {
return memberRepository.save(member);
}
}
// Config.class
@Configuration
public class Config {
@Bean
MemberRepository memberRepository() {
return new MemberRepositoryMysqlImpl();
}
}
위의 예제들은 위에서 발생한 문제들을 해결하기 위해 변경한 코드들로 하나씩 살펴보겠습니다.
- MemberRepository라는 새로운 인터페이스를 정의했습니다.
- 기존의 MemberRepositoryMysql을 MemberRepositoryMysqlImpl(Impl은 통상적으로 인터페이스의 구현체라는 의미로 붙입니다.)로 변경하고 MemberRepository를 구현하고 있습니다.
- MemberService를 보겠습니다. 기존의 MemberRepositoryMysql를 참
- 조하고 있던 코드가 이제는 구현체가 아닌 MemberRepository(Interface) 를 참조하고 있는 것을 볼 수 있습니다 그렇다면 추후에 MemberRepository를 구현한 다른 클래스로의 전환이 쉽게 이루어질 수 있다는 것을 유추할 수 있겠습니다.
- 마지막 Config 클래스는 스프링에서 빈을 등록하기 위한 또 하나의 방법입니다. 우선 @Configuration 애노테이션을 붙인 클래스는 런타임 시점에 스프링이 @Configuration이 붙은 클래스들을 찾아서 @Bean이 붙은 메소드들을 실행해서 DI 컨테이너에 스프링 빈으로 설정하는 단계를 거칩니다. 위의 예제에서는 MemberRepository에 대한 구현체로 MemberRepositoryMysqlImpl을 리턴하고 있는 것을 볼 수 있습니다.
정리하자면 Config에서 MemberRepositoryMysqlImpl을 스프링 빈으로 등록했습니다. 그렇다면 MemberService는 MemberRepository에 대한 구현체로 스프링 빈에 등록된 MemberRepositoryMysqlImpl 구현체를 주입받을 수 있고 위에서 언급한 문제점인 MsSql 구현체로 변경이 되더라도 리스코프 치환 원칙을 제대로 지키고 있다면 쉽게 구현체를 갈아끼울 수 있게 됩니다.
등록된 스프링 빈 확인하기
위의 예제에서 MemberRepository 구현체, MemberService 등을 스프링 빈으로 등록했는데, 이를 코드상에서 확인해보도록 하겠습니다.
@SpringBootTest
@Slf4j
public class BeanCheckTest{
@Autowired
ApplicationContext ac;
@Test
@DisplayName("등록된 빈 확인하기")
public void checkBeansInSpringContainer() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
log.info("beanName={} bean={}", beanDefinitionName, bean);
}
}
}
- spring.springstudy.BeanCheckTest : beanName=config bean=spring.springstudy.Config\(EnhancerBySpringCGLIB\)e9632cf4@559af296
- spring.springstudy.BeanCheckTest : beanName=memberService bean=spring.springstudy.service.MemberService@5edc3e29
- spring.springstudy.BeanCheckTest : beanName=memberRepository bean=spring.springstudy.repository.MemberRepositoryMysqlImpl@18709cb2