본문 바로가기
Spring

[Spring] 객체지향의 5가지 원칙(SOLID), 관심사의 분리

by soro.k 2022. 9. 14.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

본 포스팅은 <스프링 핵심 원리 - 기본편> 강의 내용을 복습하기 위해 간략히 작성된 글이므로 전체 내용은 위 강의에서 확인 가능합니다. 

 

1. 스프링이란

2. 객체지향의 5가지 원칙(SOLID), 관심사의 분리 

3. 제어의 역전(IoC)과 의존관계 주입(DI)

4. 스프링 컨테이너와 스프링 빈

5. 싱글톤 패턴과 싱글톤 컨테이너

6. 컴포넌트 스캔

7. @Autowired 필드명 매칭, @Qualifier, @Primary

8. 빈 생명주기 콜백

9. 빈 스코프


들어가기 전에

좋은 객체 지향 프로그래밍은 뭘까요?

자바 언어의 다형성을 활용해서 프로그래밍에서 객체를 설계하면 역할(인터페이스)과 구현(구현 객체)으로 구분하게 되고 그에 따라 코드가 유연해지고 변경도 편리해집니다. 수많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가지는데요. 이렇게 좋은 객체 지향 프로그래밍을 만들면 확장 가능한 설계이기 때문에 클라이언트에 영향을 주지 않습니다. 그러려면 인터페이스를 안정적으로 잘 설계해야 합니다.

 

참고로 실무에서는 비용을 잘 생각해야 하는데 추상화도 비용이 발생하기 때문에 기능을 확장할 기능이 없으면 구체 클래스를 직접 사용하고 향후 나중에 필요할 때 리팩토링을 통해서 인터페이스를 도입하는 것도 방법입니다. 

 

 

객체 지향 설계의 5가지 원칙(SOLID)

1. SRP : 단일 책임 원칙 (Single responsibility principle)

"한 클래스는 하나의 책임만 가져야 한다.“

 

변경이 있을 때 파급이 적으면 단일 책임 원칙을 잘 따랐다고 볼 수 있습니다.

 

 

2. OCP : 개방-폐쇄 원칙 (Open Closed Principle)

“확장에는 열려있는데 변경에는 닫혀 있어야 해.”

 

무슨 말일까요? 다형성을 생각해보면 이해가 갑니다. 인터페이스는 변경되지 않고 구현 클래스를 생성하면서 확장이 가능하다는 것이죠.

 

아래의 코드는 구현 객체를 바꾸는 코드입니다. 여기서 문제점은 무엇일까요?

public class MemberService {
	// private MemberRepository memberRepositoy = new MemoryRepository();
	private MemberRepository memberRepositoy = new JdbcMemberRepository();
}

구현 객체를 변경하려면 클라이언트 코드를 바꿔야 한다는 점입니다. 이 문제는 객체를 생성하고 연관관계를 맺어주는 별도의 설정자가 있으면 해결할 수 있습니다. 그 역할을 해주는 것이 바로 스프링 컨테이너이고요.

 

 

3. LSP : 리스코프 치환 원칙 (Liskov substitution principle)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 합니다.

 

 

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

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫습니다. 사용자 클라이언트가 있다고 치면 운전자 클라이언트, 정비사 클라이언트처럼 잘 쪼개는게 중요하다는 것이죠. 이렇게 잘 분리해 놓으면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않으니까요.

 

 

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

“추상화에 의존해야지, 구체화에 의존하면 안된다”

 

쉽게 이야기하면 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻입니다. 다시 아래의 코드를 봅시다.

public class MemberServiceImpl {
	// private MemberRepository memberRepositoy = new MemoryRepository();
	private MemberRepository memberRepositoy = new JdbcMemberRepository();
}

아까 언급했듯이 이 코드는 인터페이스에만 의존하는 것처럼 보이지만 결국은 구현 클래스를 직접 선택하는 것이기 때문에 구현 클래스도 동시에 의존합니다. 즉, 객체 지향의 핵심은 다형성이지만 다형성만으로는 OCP와 DIP를 지킬 수 없습니다. 구현 객체를 변경할 때 클라이언트의 코드를 변경해야 하기 때문이죠.

 

 

그러면 이 문제를 어떻게 해결해야 할까요?

MemberServceImpl에 MemberRepository의 구현 객체를 대신 생성하고 주입해주는 무언가를 만들면 됩니다. 직접 구현 객체를 선택하지 않도록 하는 것이 핵심입니다.

 

 

관심사의 분리(SoC, Separation of Concerns)

공연 준비를 한다고 가정해 봅시다. 그 날 공연의 배역은 누가 정할까요? 공연의 기획자 같은 제삼자가 없다면 배우들이 직접 배역을 정해야 하고 그렇게 되면 본연의 역할에 집중할 수 없겠죠. 배우는 파트너 배역에 어떤 배우가 배정되든 본인의 역할만 수행하게끔 해야 합니다. 즉, 여기에서 말하는 관심사의 분리는 객체를 생성하고 연결하는 역할과 실행하는 역할을 분명히 하는 것을 말합니다.

 

위의 DIP, OCP 원칙을 벗어난 코드를 어떻게 고칠 수 있을까요? 공연 기획자인 ‘AppConfig’를 만들어 봅시다.

 

관심사의 분리 구현 - 순수 자바 코드

// AppConfig : 구현 객체 생성
public class AppConfig {
	
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository();
	}
	
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}

	
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(), discountPolicy());
	}

	public DiscountPolicy discountPolicy() {
		return new FixDiscountPolicy();
	}
}

// MemberServiceImpl
public class MemberServiceImpl {
	private final MemberRepository memberRepository;

	public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
}

// main
public class MemberApp {
	public static void main(String[] args) {
		AppConfig appConfig = new AppConfig();
		MemberService memberService = appConfig.memberService();

		...
	}

}

AppConfig는 애플리케이션의 전체 동작 방식을 구성하기 위해서 구현 객체를 생성하고 연결해 주고 있습니다. 즉, 클라이언트 코드에서는 인터페이스만 가지고 있으면 어떤 구현 객체가 주입될지는 오직 외부의 코드, AppConfig에서만 결정된다는 것입니다.

 

이런 개념을 클라이언트 입장에서 보면 의존 관계를 마치 주입해주는것과 같다고 해서 DI(Dependency Injection, 의존관계 주입, 의존성 주입)이라고 합니다. 이제는 순수한 자바 코드가 아닌 스프링을 사용해보겠습니다. 기존에는 개발자가 직접 코드로 작성했지만 이제는 스프링 컨테이너가 그 역할을 하게 됩니다.

 

 

관심사의 분리 구현 - 스프링

@Configuration
public class AppConfig {
	
	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository();
	}
	
	@Bean	
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}

	@Bean
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(), discountPolicy());
	}

	@Bean
	public DiscountPolicy discountPolicy() {
		return new FixDiscountPolicy();
	}
}

// MemberServiceImpl
public class MemberServiceImpl {
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

// main
public class MemberApp {
    public static void main(String[] args) {
        ApplicationContext ac
            = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberSerivice memberService = 
        ac.getBean("memberService", MemberService.class);

        ...
    }
}

위의 코드에서 나오는 ApplicationContext스프링 컨테이너라고 합니다. 기존에 AppConfig가 했던 역할을 스프링 컨테이너가 해주는 것인데 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성)정보로 사용하고 @Bean이 붙은 모든 메서드를 호출해서 반환된 객체를 스프링 객체에 등록합니다.