Spring Boot 를 배우며 객체지향 이라는 개념과 DI라는 개념에 대한 의문이 들었다.
사실 Java 와 스프링 부트를 배우는 이유는, 객체 지향 언어이자, 프로그래밍을 잘 구현할 수 있기 때문이다.
그런데, SOLID 를 반영한 객체지향 프로그래밍 구현이나, DI를 통한 IDC 구현을 했다는 말 자체가 이해가 잘 되지 않았다.
목차
1. 의존의 사전적 정의
2. 기능(역할)과 구현
3. 의존의 종류(개발 세계에서의)
4. 제어의 역전이란
5. 의존성 주입이란
1. 의존의 사전적 정의
의존 또는 의존성이란 어떠한 대상에 기대고 지지하게 되는 증상을 말합니다. (나무위키)
간단하게 말해서, A 가 B를 의존한다는 것은
"A라는 객체가, B라는 객체를 사용했다" 라고 해석해도 괜찮다고 생각합니다.
결국에 , A는 B를 사용했기 때문에 관계가 주어진다고 봅니다.
B라는 객체에 메서드나, 새로운 구현이 생긴다면, A에도 영향을 끼치게 됩니다.
의존적인 관계가 형성 된다고 보여지죠.
interface와 implements.
기능과 구현에 대한 코드로 예를 들면
package hello.hellospring.repository;
import hello.hellospring.domain.Product;
import java.util.List;
import java.util.Optional;
public interface ProductRepository {
Product save(Product product);
Optional<Product> findById(Long id);
Optional<Product> findByName(String name);
List<Product> findProductAll();
}
ProductRepository 상품관련 레퍼지토리가 있습니다.
상품 관련 정보들을 받기도, 주기도 합니다.
public interface ProductRepository
코드에도 볼 수 있듯이, interface 라고 되어있습니다.
2. 기능 혹은 역할이라고 볼 수 있습니다.
ProductRepository라는 인터페이스는
상품 정보를 저장하거나, id를 통한 상품 찾기, Name을 통한 상품 찾기, 상품 전체 리스트 찾기 등의
기능을 가지고 있습니다.
그 기능을 구현했다(implements)라고 해서, 구현체라고 합니다.
그 기능을 구현하는 방법은 다양합니다.
이렇게 기능과 구현으로 나누는 이유는
구현하는 방법이 다양하기 때문입니다. (다형성 개념도 적용됨)
그리고, 회사마다 상황에 따라서, DB를 변경하거나 추가해야 할 수도 있고
회원 서비스에 대한 구현을 변경하거나 추가해야 할 수 있습니다.
유연성과 유지보수입니다.
그리고 결국엔 가독성과 생산성을 높여주기 때문에
개발 팀 전체적으로도, 플러스효과를 가져다줍니다.
구현하는 방법이 다양한 예를 들면,
흔히들 비유하는 자동차를 예로 들 수 있습니다.
자동차의 역할은 정해져있고,
그것을 구현한 구현체는 상당히 다양할 것입니다.
개발자 또한, 회원 서비스나 상품을 테스트하는 방법 등
목표로 하는 인터페이스에 대해서, 구현하는 방법이 다양할 것입니다.
(인터페이스 자체를 설계하고 , 정하는 일이 정말 중요해보이네요)
위에서 보았던 ProductRepository interface를 구현한 MemoryProductRepository를 보겠습니다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import hello.hellospring.domain.Product;
import org.springframework.stereotype.Repository;
import java.util.*;
@Repository
public class MemoryProductRepository implements ProductRepository{
private static final Map<Long, Product> productStore = new HashMap<>();
private static long sequence = 0L;
@Override
public Product save(Product product) {
product.setId(++sequence);
productStore.put(product.getId(), product);
return product;
}
@Override
public Optional<Product> findById(Long id) {
return Optional.ofNullable(productStore.get(id));
}
@Override
public Optional<Product> findByName(String name) {
return productStore.values().stream()
.filter(product -> product.getName().equals(name))
.findAny();
}
@Override
public List<Product> findProductAll() {
return new ArrayList<>(productStore.values());
}
public void clearProductStore() {
productStore.clear();
}
}
이름에서 알 수 있듯이, DB가 아닌 메모리를 통한 레퍼지토리 구현을 했다는 것을 알 수 있으며
메모리를 통해 레퍼지토리를 구현했기 때문에, DB에 저장되는 데이터가 아닙니다.
(컴퓨터가 재부팅 되거나, 웹이 reload 되면 데이터가 날라갑니다. )
중요한 것은, 상품레퍼지토리 인터페이스를 구현했다는 것입니다.
(스프링 부트에 대해서 아시는 분들은, 스프링 부트를 사용하지 않고
순수 자바로 구현되었다는 것을 알 수 있을 것입니다.)
MemoryProductRepository 는 ProductRepository 를 의존하고 있으며
ProductRepository가 변경된다면, MemoryProductRepository도 변경되어야 할 것입니다.
그래서 인터페이스를 변경하면, 구현체도 변경되어야 하기 때문에
프로젝트 초기에 인터페이스 기능에 대해 신중하게 선택해야합니다.
3. 의존의 종류
의존의 종류는 크게 4가지로 나뉩니다. 위에서 보았던 인터페이스 구현은, 4번에 해당됩니다.
1) 객체 참조에 의한 연관 관계
2) 메서드 리턴타입이나 파라미터로서의 의존 관계
3) 상속에 의한 의존 관계
4) 구현에 의한 의존 관계
1) 객체 참조에 의한 연관 관계
참조란 ? ( 참고 블로그 링크 )
reference(참조)는 프로그램이 메모리나 다른 저장 공간에서 특정 변수의 값이나 레코드에 접근할 수 있도록 하는 값이다
객체(객체변수)는 기본 데이터 변수와 다르다. 객체 변수 선언 후 메모리를 생성(new 키워드 사용)해줘야 완전한 객체가 된다.
public class StudentReference {
public int class;
public int average;
public someMethod() {
// ... 로직
}
}
StudentReference 이라는 클래스가 있습니다. 이 클래스가 선언 되었고 . 이 클래스를 참조객체 stu1에 생성해주었습니다.
StudentB 는 StudentReference 객체를 참조변수로 생성해주었기 때문에, 당연히 의존성을 가집니다.
public class StudentB {
StudentReference stu1 = new StudentReference();
private void someMethod() {
stu1.someMethod()
}
}
2) 메서드 리턴타입이나 파라미터로서의 의존 관계
말 그대로, 파라미터나 return 에서 의존성을 가지는 것을 의미합니다.
private final MemberRepository memberRepository;
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
OrderServiceImpl 는 파라미터로 MemberRepository 와 DiscountPolicy 를 받습니다.
파라미터로 클래스를 받는 것도, 의존성을 가지는 것입니다.
@Configuration
public class Appconfig {
@Bean // 등록된 메서드를, 스프링 빈으로 등록한다.
public MemberService memberService() {
return new MemberServiceImpl(getMemberRepository());
}
@Bean
public MemoryMemberRepository getMemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
getMemberRepository(),
discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
AppConfiguration 파일을 보겠습니다.
각 객체들은 return 값을 통해 객체를 반환하는 것을 확인할 수 있습니다.
이 또한 의존성을 가지는 것입니다.
3) 상속에 의한 의존 관계
먼저 상속이라는 것은 구현, 의존이라는 것은 다릅니다.
의존에는 크게 4가지 종류가 있고 , 그 중 한가지 케이스가 상속에 의한 의존입니다.
class User(AbstractUser):
# 메타 클래스는, DB 정보들에 대한 정보를 입력하는 곳
class Meta:
db_table = "user" # DB 테이블 이름을 user 로 설정해줌
email = models.EmailField(verbose_name="이메일", max_length=255,
User 라는 클래스는, AbstractUser 라는 클래스를 상속받습니다.
상속을 받게 되면, AbstractUser 에 있는 메서드나 변수들을 오버라이딩해서 재정의 할 수 있으며
AbstractUser의 성질을 갖습니다.
상속을 통해 User 와 AbstractUser 는 의존성 관계를 가지게 되고
AbstractUser의 소스 코드가 변경되면, User의 소스코드도 변경되어야 하기 때문에
User는 AbstractUser 를 의존하고 있다고 말할 수 있습니다 (User -> AbstractUser)
4) 구현에 의한 의존 관계
구현에 의한 의존 관계는, 인터페이스에 대한 구현을 그대로 보시면 되겠습니다.
위에서 ProductRepositor(인터페이스) 와 MemoryProductRepository(구현) 을 예로 설명드렸기 때문에
예시는 생략 하도록 하겠습니다.
4. 제어의 역전이란
자바에서는 Controller, Service, Repository , Domain 를 통해 MVC 패턴을 구현합니다.
구현이나 기능에 대해서는 각 폴더에서 구현하고
Configuration (전체적인 구성이나, 컨트롤)을 외부에서 하는 것을 제어의 역전 (Inversion of Control)이라고 합니다.
코드를 예로 들어 보도록 하겠습니다.
@Configuration
public class Appconfig {
@Bean // 등록된 메서드를, 스프링 빈으로 등록한다.
public MemberService memberService() {
return new MemberServiceImpl(getMemberRepository());
}
@Bean
public MemoryMemberRepository getMemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
getMemberRepository(),
discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
Appconfig 에서 discountPolicy 를 예로 들면, discountPolicy 를 호출했을 때, RateDiscountPolicy 를 리턴해주는 것을 확인할 수 있습니다. 그래서 다른 객체에서 discountPolicy를 꺼내면 우리는 RateDiscountPolicy 를 반환한다는 의미입니다.
상품 주문에 대한 인터페이스를 구현한, OrderServiceImpl 를 보면
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrive) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrive);
return new Order(memberId, itemName, itemPrive, discountPrice);
}
}
OrderServiceImpl에서
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
생성자 주입을 통해서, this.discountPolicy = discountPolicy;
discountPolicy를 this.discountPolicy 에 넣어준 것을 확인할 수 있습니다.
discountPolicy를 넣어주었지만, 우리는 AppConfig의 구성을 통해서 RateDiscountPolicy를 반환한다는 것을 압니다.
OrderService 인터페이스를 구현한 OrderServiceImpl 에서는
discountPolicy만 의존하고 있지, RateDiscountPolicy를 의존하고 있지는 않습니다.
discountPolicy가 RateDiscountPolicy인지 아닌지 모른다는 이야기 입니다.
이것이 제어의 역전(inversion of control)을 통해 얻는 강점입니다.
상품 주문 서비스는 더이상 할인 정책에 대한 configuration을 몰라도 됩니다.
그저, discountPolicy 를 의존할 뿐입니다.
이렇게 제어의 역전을 통해, 멤버에 대한 구현, 상품 주문에 대한 구현, 할인 정책에 대한 구현을
모두다 Appconfig를 통해 관리하고
그것을 스프링 부트, Bean Container에서 관리해주는데
우리는 Appconfig를 통해서 , 모든 구성을 관리하고 가독성 있고 유지보수가 편하도록 개발을 할 수 있습니다.
여기서 객체지향 프로그래밍의 SOLID 개념을 적용할 수도 있는데, 글이 길어지기 때문에, 다음 포스팅에서 집중적으로 다루도록 하겠습니다.
5. 의존성 주입이란
여기서 의존성 주입은, 생성자를 통한 주입만 다루도록 하겠습니다.
(의존관계(의존성) 주입에는 크게 4가지 방법이 있는데, 그건 다음 포스팅에서 다루도록 하겠습니다. )
생성자를 통한 주입은, 생성자라는 개념부터 알아야 하는데
- 생성자(Constructor)란 ? (나무위키 : 생성자)
constructor
객체 지향 프로그래밍에서 객체가 생성될 때 초기화시켜주는 함수를 의미한다. 필요에 따라 객체 내 데이터에 특정한 값을 입력하기도 한다.
C++과 자바같은 언어에서는 클래스와 동일한 이름을 가진 함수가 생성자로서 기능한다. 그 외 파이썬이나 Objective-C같은 언어에서는 특정한 키워드가 따로 정의되어있다.
생성자는 자료형을 갖지 않는다. void조차 아니다.(나무위키)
class Game{
private:
string title; // 게임의 제목을 나타낸다
int price; //게임의 가격을 나타낸다.
}
int main(){
Game Minecraft; //Minecraft라는 이름의 인스턴스가 생성
}
Game 이라는 클래스에 대해서
main 함수는, Game 이라는 클래스 이름과 똑같은, 생성자 Game 을 통해서
Minecraft 라는 이름의 인스턴스를 생성하는 것을 볼 수 있습니다.
이것을 생성자라고 합니다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
위와 똑같이, OrderServiceImpl 에서는
public OrderServiceImpl 이라는 생성자를 통해서
this.memberRepository 와 this.discountPolicy 에
memberRepository 와 discountPolicy 를 주입한 것을 볼 수 있습니다.
C++과 자바같은 언어에서는 클래스와 동일한 이름을 가진 함수가 생성자로서 기능합니다.
첫번째 줄에서 OrderServiceImpl 는 클래스 이름이고
다섯번째 줄에서 OrderServiceImpl 는 생성자를 의미합니다.
OrderServiceImpl 는 생성자를 통해서,
memberRepository와 discountPolicy 를 의존관계 주입하였습니다.
참고한 자료와 강의는
강의
김영한의 스프링 입문 - 코드로 배우는 스프링 부트, 웹MVC, DB 접근기술
김영한의 스프링 핵심 원리 - 기본편
[Wanted 프리온보딩] 1월 백엔드 첼린지 - 객체지향프로그래밍이란? (Zayden멘토님)
블로그, 나무위키
생성자란?
위 글에 반영된 링크들 ....
자바 및 스프링 공부를 하면서
의존, DI, 객체지향 프로그래밍에 대한 이해도와 기본 지식이 굉장히 중요하다는 것을
유튜브나, 강의를 통해 접해보니
이렇게 따로 블로그에 기록해야겠다는 생각이 들었습니다.
이후 객체지향 프로그래밍의 원칙 SOLID와
의존 관계 주입의 4가지에 대해서 포스팅 하도록 하겠습니다