게시글
블로그
전체 42024. 10. 20.
0
[워밍업 클럽 2기 BE 클린코드&테스트 코드] 3주차 발자국
강의에 대한 리뷰드디어 기다리던 테스트코드 작성 강의를 수강했다.강의 내용테스트 코드 작성법부터, Junit5, TDD, BDD 등 다양하게 배웠다. 중요한 건 테스트하기 좋은 코드와 그렇지 않은 코드를 구분하는 것 이라 생각한다. 나는 이것을 잘하는 게 SOLID 원칙을 지키는 것과 유사하며, 확장성있는 설계, 유지보수하기 쉬운 설계에 가까워진다고 생각한다. 실제로 적용하기에는 많은 연습이 필요하겠지만, 마스터(?)하게 되면 좀 더 좋은 코드를 작성할 수 있다는 것만큼은 확신할 수 있다.그리고 예제를 중심으로 스프링에서 테스트 코드를 레이어별로 어떻게 쌓아가는 지 알아봤다. 예제는 JPA 기반이었으며, 리포지토리는 새로 추가된 리포지토리만 테스트해봤다. 내가 알기로도 리포지토리 테스트는 @DataJpaTest 가 가볍기 때문에 다들 사용하는 것으로 알고있는데, 우빈님은 선호하지 않는다고 하신다. 그 이유는 뒤에 강의에서 나오는데 통합테스트할 때, 모든 테스트를 실행하게 되면 Spring boot가 너무 여러번 뜨게된다. 이 행위는 시간을 잡아먹기 때문에 비용이다. @DataJpaTest를 사용하면 그 볼륨은 작지만, 다른 레이어를 사용하면서 @SpringBootTest 를 사용했다면, 이미 띄운 서버를 그냥 같이 써버리는 게 훨씬 경제적이라는 맥락이다. 나는 제대로 테스트 코드를 작성해본 적이 없어서 엄청난 공감을 하진 못한다. 써보면서 테스트 비용에 대해 알아가야겠다.미션Readable Code에서 했던 예제 중 하나를 선택해서 테스트코드를 작성하는 미션이었다.나는 스터디카페 이용권 예제를 선택했다. 다음은 테스트를 작성하면서 정리한 내용이다.테스트 하기 어려운 코드IO 작업을 하는 코드요구 사항이 변하기 쉬운 코드. ex) 사물함 사용가능한 좌석 이용권ex) 비즈니스 로직에서 사용되는 상수에 따라 결과가 달라지는 코드테스트 하기 쉬운 코드input과 output이 명확한 코드의존성을 외부에서 주입할 수 있는 코드단위가 작은 메서드, 객체이것도 테스트를 해야하나? 하는 코드생성자, 정적 팩터리 메서드단순 getter 메서드단순 위임 메서드 (위임의 가장 안쪽 클래스는 테스트O)기타 어려웠던 점DisplayName에 나타낼 테스트명을 작성하는 것이 어려웠음비즈니스 코드를 리팩터링하고 싶은 코드들이 있었음. 우선 미션 외적인 것이라 생각해 손대지 않음. 회고이번주 강의 내용은 거의 실습위주라 이론적으로 정리할 게 별로 없었다. 조금 달렸던 일주일인 것 같은데, 다른 것도 막 병행하느라 놓친부분이 있진 않을까 걱정된다. 할 건 너무 많은데 이래저래 머리가 복잡하고 마음이 급하다.커리어를 길게보고 좀 더 여유를 가지자.
2024. 10. 13.
0
[워밍업 클럽 2기 BE 클린코드&테스트 코드] 2주차 발자국
강의에 대한 리뷰지난 주에 이어서 Readable Code를 진행했다.코드 다듬기, 리팩토링 연습, 기억하기 좋은 조언들, Outro 섹션을 진행하였고 Readable Code 강의는 마무리 되었다.저번 주 학습한 내용은 Readable Code를 어떻게 구현하는 가에 가까웠다면, 이번 주 학습 내용은 잘 정리된 Readable Code를 좀 더 정갈하게 정리해보자. 라는 느낌이었다.예를 들면, 섹션 6. 코드 다듬기에서는 주석의 양면성과 메서드 나열 순서, 패키지 나누기 등을 학습한는데, 내 생각에 이 주제들은 구현보다는 정리이기 때문에 강사님도 코드 다듬기라고 한 것이라 추측한다.강의 내용주석이 많다는 것은 비즈니스 요구사항을 어떤 방법으로든 코드에 잘 녹여내지 못했다는 뜻이다. 하지만 코드에는 도저히 녹여낼 수 없는 것들도 존재한다. 예를 들어 해당 코드에 대한 의사 결정 히스토리는 코드에 담아내기 어렵다. 나 역시 실무에서 레거시 코드를 봤을 때, "이렇게 하면 쉽고 깔끔할 것 같은데 왜 이렇게 복잡하고 어렵게 했지?" 라는 생각으로 리팩터링할 때가 종종있다. 이후에, 알고보니 그렇게 짜놓은 이유가 있는 코드였었다. (물론 10번 중에 한 두 번만..)클린코드와 주석에 대한 의견이 꽤 분분하다고 알고 있다. 모든 클래스와 메서드에 주석을 작성하기를 권장하는 사람들도 있는 반면, 이를 모두 코드에 담아내라는 사람들도 존재한다. 나는 아직 경험이 적기 때문에 정답은 잘 모르겠다. 하지만 자바, 스프링 등 구현된 소스를 보면 클래스 단위에 주석이 작성되어 있는 것이 좀 더 공부하기 좋았다. 주석은 상황, 팀 컨벤션 등에 따라 선택하면 될 것 같다. 또, 메서드 나열 순서에 대해서도 배웠는데, 이건 정답이라기 보다 뭐랄까... 좋은 습관 정도라고 생각한다. 생각해보면, 메서드의 순서는 그 순서를 알고 있는 사람에겐 Readable 하지만, 모르는 사람에겐 아무 도움도 되지 않는다. 어찌됐든 혼자 하든 같이 하든 메서드 나열 순서에 대한 컨벤션이 있으면 Readable 하겠다 라는 생각은 한다. 다음은, 기억하면 좋은 조언들 섹션에 대해 얘기해보려 한다.그 중, 오버 엔지니어링과 은탄환은 없다 섹션을 감명깊게 봤다. 최근에 내가 시도하려했던 사이드 프로젝트가 오버 엔지니어링으로 흐지부지 된 경험이 있다. 정확히 말하자면 오버 엔지니어링을 시도하다가 흐지부지 됐다고 하자. 클린코드나 Readable Code에 대한 건 아니고, 개발 하기도 전에 이것저것 (Sonarqube, Jacoco, Jenkins, Spring security...) 다 붙여놓은 다음에 개발하려다가 개발을 제대로 해보지도 못하고 지쳐서 흐지부지 되어버렸다. 작동하는 작은 단위부터 하나씩 쌓아올려야 한다는 점을 다시 되새겼다. 테스트 코드 작성에 대한 강의를 듣고 다시 시도해볼 것이다. 미션이번 주는 미션이 하나밖에 없었다.스터디카페 키오스크에 대한 코드를 지금까지 배운 것을 토대로 리팩터링하는 것이었다.가장 어려웠던 것은 추상화 레벨에 대한 것이다. 아직 추상화 레벨을 맞추는 게 어렵다. 추상화 레벨의 기준이라는 것도 잡기 어렵다. 이 부분은 꾸준히 생각하며 개발하다 보면 코드에 대한 사고가 변할 거라 생각한다. 회고나는 무엇이든 진득하게 못하는 것 같다. 다음 할 일이 있으면 그것을 위해 이전에 하던 것을 최대한 빠르게 마무리하고 넘어가려는 성향이 있다. 이런 성격만 바꾸면 조금 더 좋은 사람, 더 좋은 개발자가 되리라고 확신하는데 알면서도 그게 잘 안 된다. 이 부분을 바꿀 수 있는 방법에 대해 좀 더 고민해보고 알아봐야겠다.
2024. 10. 06.
0
[워밍업 클럽 2기 BE 클린코드&테스트 코드] 1주차 발자국
시작사실 마감 직전까지 이런 스터디가 있는 줄도 몰랐다. 인프런에서 볼 수 있는 Josh Long의 Spring Boot 밋업이 재밌길래 그걸 보느라 인프런을 들락날락 하다가 우연히 워밍업 클럽 배너를 봤다. 최근 나의 학습 패턴도 예전같지 않고, 마음가짐도 점점 시들어가는 걸 느끼는 와중이었다. 처음에는 CS 스터디를 신청하려고 했다. 다시보니 스프링에서의 테스트 코드 작성에 관한 스터디가 있었기에 바로 노선을 변경했다. 나는 올해 초, 다른 교육기관에서 클린코드와 리팩터링, TDD에 대한 과정을 수료했다. 그 과정에서 배운 점이 많고 지금도 실무에 많이 적용한다. 굉장히 좋은 과정이었지만, 딱 하나 아쉬운 점이 있었다. 해당 과정은 순수 자바로만 진행된다는 것이다. 클린코드와 리팩터링은 순수 자바만으로도 내 실무에 바로 적용할 수 있었다. 하지만 TDD는 그렇지 않다. TDD 그 자체는 깨우쳤는데, 내 실무 코드는 스프링 프레임워크 위에서 논다. 결국, 스프링 기반 프로젝트에서 TDD를 하는 방법은 순수 자바 TDD와는 꽤 많이 달랐다. Layer 별로 개발해야하고, DB 테스트, Mocking 등 다양한 상황과 변수들이 있었고 배운 걸 써먹기에는 한계가 있었다. 이번 스터디를 진행하며, 이전에 배운 클린코드를 복기하고 스프링을 사용한 TDD를 학습하고자 한다.1주차 학습1주차에는 추상과 구체, 사고를 줄이는 코드 작성법, 객체지향적인 코드(SOLID)에 대해 학습했다. 클린 코드 파트는 내가 올해 초에 수료한 교육기관의 과정이 지금 우빈님의 수업과 굉장히 유사하다. 그도 그럴 것이, 두 분 다 우아한테크코스에 관련되어 계시기 때문에 커리큘럼과 추구하는 클린 코드가 비슷할 것이라 생각한다. 덕분에 내가 배웠던 것을 잘 복기하고 있다. 추상1주차에서 가장 중요하다고 생각하는 부분은 추상에 대한 고찰이다. 컴퓨터는 끝없는 추상의 연속이다. 나 역시 개발자는 끊임없이 무언가를 추상화하는 직업이라고 생각한다. 그렇기 때문에 더더욱 추상과 구체에 대해 이해해야한다. 구현체에서 인터페이스를 뽑아내는 것, 변수나 메서드의 이름을 잘 짓는 것도 추상화이다. 구체에서 어떤 개념을 뽑아내는 것이 추상이기 때문에, 추상화된 것들을 보면 구체가 뭘 할 지 대충 예상할 수 있어야 한다. 메서드로 예를 들어보자. 메서드는 메서드 시그니처(메서드 명과 매개 변수 타입&개수)와 구현(내부 코드)으로 나뉘어져 있다. 메서드 시그니처만 보고, 어떻게(How) 구현되어 있는 지 알 수는 없지만 무엇을(What) 해줄 것인 지는 유추할 수 있어야한다. 그렇지 못한 메서드 시그니처는 추상화에 실패한 것이며 이를 사용할 팀원, 미래의 나에게 무례한 것이다. 클린 코드강의에서는 뇌 메모리 적게 쓰기. 즉, 코드를 읽는 사람으로 하여금 기억해야할 컨텍스트를 줄이는 것이 중요하다고 한다. 이 부분은 매우 동의하고 강의에 나온 것들 모두 실무 개발할 때에도 적극적으로 활용하고 있는 부분들이다. Early Return, 사고의 depth 줄이기, 공백 라인을 대하는 자세, 부정어를 대하는 자세, 해피 케이스와 예외 처리 이다.개인적으로 Early Return과 사고의 depth 줄이기 부분은 조금 조심해서 적용해야한다고 생각한다.Early Return 이전에도 Early Return에 대해 학습한 적이 있어서, Early Return을 할 수 있으면 최대한 사용한다. 그런데 커리어 내내 if, else-if, else 만을 사용해왔던 내 상사는 이 부분을 잘 이해하지 못한다. 작성한 코드를 곰곰이 쳐다보면, 그냥 else를 사용하는 게 이해하기 쉽겠는데? 하는 부분들이 있다. 또한, 강의에서도 우빈님이 else를 불가피하게 써야할 때가 있다는 뉘앙스로 말씀하셨고, 나 역시 너무너무 억지로 Early Return을 하는 것은 그리 좋은 습관인 것 같지 않다.사고의 depth 줄이기이 부분도 Early Return과 비슷한 맥락인데, 억지로 depth를 줄이려고 불필요한 메서드를 만들거나 스트림을 사용하게되는 경우가 많다. 스트림을 적극 활용하는 것이 분명 자바 생태계에서 일반적으로 좋지만, 스트림을 잘 모르는 개발자들에게는 조금 난해할 수도 있다. 후배 개발자라면 같이 공부하겠지만 상사가 스트림을 모르면 난처하다..ㅎㅎ SOLID객체지향하면 SOLID는 빼놓을 수 없다. 이에 대해 자세하게 학습하였고, 각각의 원칙을 기존 코드를 리팩터링하며 코드로 적용해보았다. SOLID에 대한 내용은 미션에서 작성하였으므로 생략한다. 객체지향 적용하기상속과 조합, VO, 일급 컬렉션, Enum 등을 학습하였다. 이 파트는 코드 따라치기가 분량이 너무 많아서 조금 힘들었다. 그래도, 조합에 대해 알아보고 VO, 일급컬렉션, Enum 등 아는 것들을 복기해볼 수 있는 좋은 파트였다. 나도 Enum 사용하는 것을 매우 좋아하는데, Enum을 구현체로 만드는 것은 해보지 못했다. 또, Enum 각각의 구현체를 인라인으로 생성하여 오버라이드하는 것 역시 신기한 방법이었다. Enum의 원리에 대해 다시 공부해보려고 한다. 미션이번 주 미션은 총 두 개 였으며, 무엇이든 추상화된 것을 구체화해보는 것과, SOLID를 자기 언어로 표현 + 코드 리팩터링이다. 추상화된 것을 구체화내가 퇴근하면서 생각한 것이라, 퇴근 그 자체를 구체화해봤다. 우리가 퇴근이라고 하면 어쨌든 업무를 마무리하고, 근무지에서 나와서 집으로 향하는 것을 말한다. SOLID를 자기 언어로 표현 + 코드 리팩터링해당 미션은 인프런 블로그에 좀 자세하게 길게 썼기 때문에 중복된 내용일 것 같다. 개발자는 중복을 제거하는 것도 중요하기 때문에 중복 제거를 위해 작성하지 않고 링크만 첨부하겠다. https://www.inflearn.com/blogs/8370아직 1주차라 미션이 꽤 간단했다고 생각한다. SOLID에 대해 다시 고민해보는 것은 꽤 좋은 미션이었다. 취준할 때나 생각하지, 실무에 들어선 이상 SOLID를 지켜가며 개발하긴 어렵다. 하지만 사실 SOLID를 지켜야 리팩터링하기에도 테스트하기에도 갑자기 바뀌는 요구사항에 대응하기에도 좋은 코드라는 것을 너무나도 잘 알고있다. 이번 미션을 계기로 실무에서도 SOLID를 생각해보려고 한다.
2024. 10. 03.
0
워밍업 클럽 2기 BE 클린코드&테스트 코드 DAY 4 미션
1. 아래 코드와 설명을 보고, [섹션 3. 논리, 사고의 흐름]에서 이야기하는 내용을 중심으로 읽기 좋은 코드로 리팩토링해 봅시다.public boolean validateOrder(Order order) { if (order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; } else { if (order.getTotalPrice() > 0) { if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } else { return true; } } else if (!(order.getTotalPrice() > 0)) { log.info("올바르지 않은 총 가격입니다."); return false; } } return true; }섹션 3에서 강조하는 것은 뇌 메모리 적게 쓰기. 즉, 코드를 읽는 사람으로 하여금 생각을 많이하지 않도록 하는 것이다. 그 방법에는 여러가지가 있는데, 순서대로 수정하면서 알아보자.if (order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; }우선 첫 줄부터 마음에 들지 않는다. 코드의 의도를 보니 주문(Order ) 내의 상품(Item) 컬렉션의 요소 개수가 0개인 지 확인하는 것으로 추측된다. 해당 코드에 Order , Item 객체에 대한 내용은 없으니 해당 클래스도 수정한다는 가정하에 진행하겠다. Items를 꺼내지 말고, Order 클래스에 Item 컬렉션이 비었는 지확인하는 메시지인hasNoItems() 을 만들어 다음과 같이 수정해보자.if (order.hasNoItems()) { log.info("주문 항목이 없습니다."); return false; }좀 더 직관적이고 객체지향적으로 수정되었다. 하지만 아직 마음에 들지 않는 것이 있는데, 바로 다음에 나올 else이다. 주문 항목이 없다면 return false 로 해당 메서드를 빠져나가기 때문에 다음 else가 있을 필요가 없다. 메서드에서 if와 else 가 공존하려면 if-else 뒤에 해당 if문에서 분기된 코드가 if-else 뒤에서 사용되는 경우밖에 없다. Early Return을 해보자. 사실 이미 Early Return이긴 하다.. else만 없애보자.public boolean validateOrder(Order order) { if (order.hasNoItems()) { log.info("주문 항목이 없습니다."); return false; } // 주문 항목이 없으면 Early Return! if (order.getTotalPrice() > 0) { // (1) if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } else { return true; } } else if (!(order.getTotalPrice() > 0)) { // (2) log.info("올바르지 않은 총 가격입니다."); return false; } return true; }else 문이 한 레벨 사라졌기 때문에 전체적인 depth가 1 줄었다. 하지만 depth가 최대 2이므로 직관적인 사고를 하기에는 피곤하다. 남은 코드들도 depth를 줄여보자. 코드(사고)의 depth를 줄이는 데에는 Early Return이 최고다. 위에서도 봤지만, Early Return을 하면 굉장히 직관적으로 코드를 읽을 수 있게 된다. (1), (2) 분기문이 있는데, (1)은 아직 2 depth 이기 때문에 (2) 부터 Early Return 하는 게 쉽겠다.public boolean validateOrder(Order order) { if (order.hasNoItems()) { log.info("주문 항목이 없습니다."); return false; } // 주문 항목이 없으면 Early Return! if (!(order.getTotalPrice() > 0)) { // (2) log.info("올바르지 않은 총 가격입니다."); return false; } if (order.getTotalPrice() > 0) { // (1) if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } else { return true; } } return true; }(2)를 Early Return을 하고 보니 분기 조건이 굉장히 거슬린다. 부등호를 굳이 Not 연산을 사용해서 한 것부터 약간 킹받는다. getter로 객체를 조회하여 비교하지말고, 메시지를 보내보자. if (order.isTotalPriceNegative()) { // (2) log.info("올바르지 않은 총 가격입니다."); return false; }주문이 음수인 지 확인하는 메서드(메시지)를 만들어 Order 객체로부터 정보를 얻자. 이제 (1)을 손볼 차례다. 우리가 (2)를 Early Return 하면서 (1)의 가장 최상위 분기는 의미 없어졌다. (2)를 통해 Early Return 되었다면 Order의 Total Price는 0 이상이라는 것이기 때문이다. if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } else { return true; }이제 (1)도 depth가 1이 되었다. 하지만 여기서도 Early Return으로 else 를 없앨 수 있다. depth를 최소화하면서 Early Return을 적용한 코드를 확인해보자.public boolean validateOrder(Order order) { if (order.hasNoItems()) { log.info("주문 항목이 없습니다."); return false; } // 주문 항목이 없으면 Early Return! if (order.isTotalPriceNegative()) { log.info("올바르지 않은 총 가격입니다."); return false; } // 총 가격이 0 이하이면 Early Return! if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } // 사용자 정보가 없으면 Early Return! return true; }정리하면서, 각 if문 사이에 공백을 한 줄씩 추가했다. 이렇게 논리 사이에 공백을 주는 것은 생각의 컨텍스트를 분리시킬 수 있는 좋은 방법이다. 예를들어 주문 항목이 없는 것과 가격이 0 이하인 것은 전혀 다른 문제이기 때문에 굳이 줄을 붙여서 사고를 연결할 필요가 없다.거의 다 마무리 됐다 싶었는데 부정 연산 `!` 하나가 거슬린다. 부정 연산은 읽는 사람으로 하여금 한 번 더 생각하게 만든다. if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; }이 코드의 분기 조건을 읽자하면, 주문에 사용자 정보가 있는 지 확인하고 그것을 뒤집어서 사용자가 없는 지를 확인하는 게 되는데.. 이미 말로만 해도 복잡하고 귀찮다. ! 는 될 수 있으면 최대한 사용하지 않고 메서드명으로 추상화하자. if (order.hasNoCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; }모든 작업이 완료되었다. 1번 미션의 최종 코드를 확인해보자.public boolean validateOrder(Order order) { if (order.hasNoItems()) { log.info("주문 항목이 없습니다."); return false; } if (order.isTotalPriceNegative()) { log.info("올바르지 않은 총 가격입니다."); return false; } if (order.hasNoCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } return true; }2. SOLID에 대하여 자기만의 언어로 정리해 봅시다.Robert.C.Martin의 SOLID는 어쩌면 캡추상다 보다 중요한 객체지향 이론이 되었다. 하나씩 살펴보자.SRP (Single Responsibility Principle)단일 책임 원칙. 하나의 객체는 하나의 책임만을 가져야한다는 뜻이다. SRP에서 가장 중요한 것은 객체를 변경해야할 사유가 단 하나라는 것이다. 개인적으로 SOLID 원칙 중에 가장 직관적이고 쉬운 원칙이라고 생각한다. 하지만 실제로 개발하다보면 가장 지키기 어려운 것이 또 SRP이다. 객체지향프로그래밍은 여러 객체가 각각의 책임을 가지고 협력하는 시스템이다. 책임을 각각 가져야한다. 내 책임이 아니면 다른 객체에게 이관하자.OCP (Open-Closed Principle)개방 폐쇄 원칙. 확장에는 열려있고 수정에는 닫혀있다는 원칙이다. OCP는 추상화를 통해 코드의 변경이 아닌 확장이 중요하다. 뒤에 나올 DIP와 OCP가 꽤 맞물려 있다고 생각한다.LSP (Liskov Substitution Principle)리스코프 치환 원칙. 상속 관계에서 자식클래스는 부모 클래스의 역할을 할 수 있어야 한다. 예를 들어, 부모 클래스의 run()은 스레드를 실행시키는 메서드인데, 자식 클래스의 run()은 육상 경기를 시작하는 메서드이면 안 된다. 그렇게 되면 해당 클래스를 참조하는 클라이언트 코드 입장에서 더 이상 그 클래스의 모든 족보를 신뢰하지 못하고 난처해진다.ISP (Interface Segregation Principle)인터페이스 분리 원칙. 어떤 인터페이스의 구현체는 사용하지 않는 메서드까지 구현할 필요가 없다. 그 말은 즉, 인터페이스가 쓸 데 없이 너무 광범위한 메서드까지 추상메서드로 가지고 있다는 것이다. 추상화는 구현으로 부터 공통 개념을 뽑아내는 것이다. 그렇다면 구현이 우선일까 추상이 우선일까? 메서드를 누구에게 맞춰야할까?DIP (Dependency Inversion Principle)의존성 역전 원칙. 구현(저급 모듈)이 아닌 추상(고급 모듈)에 의존하라는 것이다. OCP와 맞물려 Spring 프레임워크에서 매우 중요한 원칙이라고 생각한다. A 객체가 B 객체를 의존할 때, B 객체의 저수준인 구현체를 의존하게 되면 구현체를 바꿀 때 마다 A 객체의 코드를 수정해야 한다. 이 행위는 OCP를 지키지 못하는 모습도 된다. DIP를 위해 B 객체의 추상화된 고수준 모듈(인터페이스, 추상클래스, 부모 클래스)을 의존하고, 외부에서 A 객체를 생성할 때 B 객체의 실제 구현체에 대한 의존성을 주입(DI)하게 되면 A 객체를 B 객체 때문에 변경할 일은 없을 것이다.