객체지향 프로그래밍이란 프로그램을 객체들의 협력과 결합으로 파악하고자 하는 프로그래밍의 패러다임이다.
그렇다면 어떻게 해야 객체 지향적으로 설계를 잘할 수 있을까?
2000년대 초 로버트 마틴이 제안한 SOLID 원칙이란 것이 있다.
하지만 이 원칙에 따라 코드를 짜다보면 OCP와 DIP에서 문제가 발생한다.
public interface Product {
public abstract void setPrice(int price);
public abstract int getPrice();
}
-----------------------------------------------
public class Pencil implements Product {
int price;
@Override
public void setPrice(int price) {
this.price = price;
}
@Override
public int getPrice() {
return price;
}
}
public class NoteBook implements Product {
int price;
@Override
public void setPrice(int price) {
this.price = price;
}
@Override
public int getPrice() {
return price;
}
}
-----------------------------------------------
public class Store {
//private Product product = new NoteBook();
private Product product = new Pencil();
}
위 코드에서 Store가 Pencil이라는 구체화에 의존하고 있기 때문에 DIP를 위반하고 있다.
또한 다른 Product로 바꾸기 위해서는 Store의 코드를 고쳐야 하기 때문에 OCP도 위반하고 있다.
이 의존성 문제를 해결하기 위해서는 Store와 Pencil의 연결을 끊어 줘야한다.
하지만 그렇게 된다면, Pencil을 생성이 불가능하기 때문에 NullPointerException이 발생한다.
따라서, 위 그림처럼 누군가가 객체를 대신 생성을 해서 주입을 시켜줄 필요가 있다.
외부에서 두 객체 간의 관계를 결정해 주는 디자인 패턴으로,
위 그림처럼 인터페이스를 사이에 둘 때 객체가 구체화에 의존하지 않게 해주고,
런타임 시에 관계를 동적으로 의존성을 주입해
프로그램의 유연성을 높여주고 결합도를 낮춰 객체 지향적으로 만들어 준다.
장점
1. 코드가 간결하다.
단점
1. 외부에서 접근이 불가능하다.
2. 프레임워크가 무조건 있어야 사용이 가능하다.
3. 프레임워크에 의존적이기 때문에 객체지향적이지 않다.
@Controller
public class MyController {
@Autowired
private MyService myService;
}
단점
1. Setter를 public으로 열어두어야 하기 때문에 주입된 객체가 언제든지 변경될 가능성이 있다.
@Controller
public class MyController {
private MyService myService;
@Autowired
public void setService(MyService myService) {
this.myService = myService;
}
}
--------------------------------------------------
public class Store {
private Product product;
public void setProduct(Product product) {
this.product = product;
}
}
장점
1. 생성자의 호출 시점에 1회 호출 되는 것이 보장한다.
2. 주입받은 객체가 변경될 가능성이 없다.
3. 생성자가 1개만 있을 경우에 @Autowired를 생략해도 주입이 가능하다.
@Controller
public class MyController {
private MyService myService;
//@Autowired
public void MyController(MyService myService) {
this.myService = myService;
}
}
--------------------------------------------------
public class Store {
private Product product;
public Store(Product product) {
this.product = product;
}
}
스프링 공식 문서에서 생성자 주입 방식 사용을 정식으로 권고하고 있는데 그 이유는 다음과 같다.
생성자 주입의 경우 애플리케이션 구동 시점에서 순환 참조 에러를 감지할 수 있다.
즉, Bean에 등록하기 위해 객체를 생성하는 과정에서 순환 참조가 발생한다.
@Service
public class NewService {
@Autowired
private MyService myService;
}
-------------------------------------------
@Service
public class MyService {
@Autowired
private NewService newService;
}
생성자 주입 시 final 선언이 가능하기 때문에, 런타임 시 주입받는 객체가 변할 가능성이 없다.
@Controller
public class MyController {
private final MyService myService;
//@Autowired
public void MyController(MyService myService) {
this.myService = myService;
}
}
테스트 코드를 좀 더 편리하게 작성이 가능하다.
public class MyService {
private final MyRepository myRepository;
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
public void testFuntion() {
//테스트 할 기능
}
}
public class MyServiceTest {
MyRepository myRepository = new MyRepository();
MyService myService = new MyService(myRepository);
@Test
public void test() {
myService.testFuntion();
}
}
https://mangkyu.tistory.com/150
https://mangkyu.tistory.com/125
https://dev-coco.tistory.com/70