🔥딱 8일간! 인프런x토스x허먼밀러 역대급 혜택

블로그

스프링 핵심 원리 - 기본편 / 객체 지향 설계 / DI와 정적, 동적 의존 관계

스프링 핵심 원리의 기본편을 수강중입니다. 이론에 해당되는 내용 중 일부를 재해석해서 정리해보았습니다.용어 정리의존 관계이 글에서는 일단 "A가 B에 의존한다"를 "B가 변경될 경우 A가 변경되어야 한다"라고 정의하겠습니다.예를 들어 어떤 프로젝트 내에서 데이터 저장소를 ARepository에서 BRepository로 변경할 때 CService의 코드 내에 해당 부분을 변경해야 할 경우, "CService 클래스 구현 코드는 ARepository 클래스에 의존한다"고 표현하겠습니다. 정적 / 동적이 글에서만 관심 있는 영역의 코드가 컴파일되기 전에 이미 고정된 것을 정적인 것으로 정의하고, 코드 상으로는 고정되어 있지 않고, 컴파일 후 실행될 때에야 정해지는 것을 동적인 것으로 정의하겠습니다.다른 글들을 보았을 때 맥락에 따라 "컴파일" 대신 다른 단어가 들어오는 경우도 있는 것 같습니다.OCP (개방-폐쇄 원칙)"코드는 기능 확장에는 열려 있으면서, 수정에는 닫혀 있어야 한다."는 원칙입니다.다시 말해 코드 자체를 수정하지 않고도 기능을 확장할 수 있어야 한다는 원칙입니다. SRP (단일 책임 원칙)모듈은 한 가지 책임만 지녀야 한다는 원칙입니다.여기서 책임이란 모듈의 변화에 대한 이유입니다.이 글의 맥락에서는 지엽적인 내용이지만, 솔직히 말씀드리면 위 정의를 보긴 했지만 적용이 가능할 정도로 명확히 알지 못해 다음 자료들을 참고하였습니다. 제가 일단 이해한 내용을 공유드리겠습니다.일단 이해한 내용소프트웨어를 개발할 때 사용하는 자료 구조나 알고리즘의 종류와 같이 변화가 예상되는 부분들이 있다. 소프트웨어를 모듈로 나타낼 때, 실행되는 순서로 모듈을 구성하기보다는 변화하는 부분들을 다른 모듈들로부터 격리시키는 방식으로 모듈화를 한다면 특정 요소가 변하더라도 그 변화에 대한 한 모듈만 수정할 수 있다.비즈니스 소프트웨어에서는 변화를 요구하는 대상이 특정 비즈니스 로직과 관련된 특정 부서이기 때문에, 모듈을 설계할 때 그 모듈에게 변화를 요청하는 대상이 한 부서가 되도록 구성하는 것이 이상적이다.참고 자료https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.htmlhttps://en.wikipedia.org/wiki/Single-responsibility_principlehttps://dl.acm.org/doi/pdf/10.1145/361598.361623https://www.sicpers.info/2023/10/ive-vastly-misunderstood-the-single-responsibility-principle/DI (의존성 주입)해결하고자 하는 문제어떤 기능을 실행 시에 다양한 구현체들을 사용하는 방식으로 확장하고 싶습니다.예시: 데이터 저장을 위한 Repository를 운영 시에는 DatabaseRepository를 사용해서, 개발 시에는 InMemoryRepository를 사용하여 실행하고 싶습니다.그러나 Repository 구현체를 변경하려고 보니 Repository를 사용하는 모든 곳에서 구현체를 수정해야 하는 문제가 발생했습니다.예시 class Service { private final Repository repository = new InMemoryRepository(); // 코드 수정 필요 // Repository 사용 로직 ... }DI를 사용한 해결 방안Repository를 사용하는 클래스들이 DatabaseRepository와 같은 구체적인 구현체들을 사용하지 않게 지워버린 후 Repository 인스턴스를 외부에서 주입받도록 합니다. 여기서는 생성자를 통해 주입받았습니다.예시 class Service { private final Repository repository; public Service(Repository repository) { this.repository = repository; } // Repository 사용 로직 ... }Config 클래스를 따로 두어 인스턴스를 생성하는 코드들을 모두 이 클래스에 둡니다. 인스턴스가 필요한 경우 이 클래스에서 사용하면 됩니다. 예시class Config { repository() { return new InMemoryRepository(); } service() { return new Service(repository()); } }OCP 관점에서의 해석원래 문제에서는 실행 시점에 다른 구현체를 사용하는 기능 확장을 하고 싶었지만, 이를 위해 구현체를 사용하는 코드를 변경해야 했습니다. 따라서 OCP를 위반하는 설계였습니다. 이 때 OCP의 각 부분을 이 맥락에서 다음과 같이 해석할 수 있습니다.기능 확장의 대상은 실행 시점에 실행되는 코드로, 이는 Repository 클래스의 인스턴스입니다.반면 소스 코드 수정과 관련된 부분은 컴파일 시점에 결정되는 Service 클래스의 코드 자체입니다.따라서 OCP를 만족하기 위해서는 "Service 인스턴스의 실행 시점에는 서로 다른 Repository 구현체 인스턴스를 사용하되, 컴파일 시점에 같은 Service클래스 코드를 사용하고 싶다"는 문제를 해결해야 합니다.다시 말하면 각 Service 인스턴스는 서로 다른 Repository 구현체 인스턴스에 의존하게 하되, Service 클래스는 구체적인 Repository 구현체 클래스에 의존하지 않도록 해야 합니다.그래서 DI를 사용해서 Service와 Repository 간의 동적인 의존관계와 정적인 의존관계를 서로 다르게 만듦으로써 OCP를 만족하는 설계를 구성하였다고 해석할 수 있습니다. SRP 관점에서의 해석기존의 Service 코드에서는 Repository의 인스턴스를 생성하는 책임과 Repository를 사용하는 책임이 같이 있었습니다. 따라서 SRP를 위반하는 설계였습니다.이 때 Repository를 사용하는 로직은 Repository 인터페이스만 알고 구현체 클래스를 몰라도 문제가 없습니다.반면 Repository 인스턴스를 생성하려면 구체적인 Repository 구현체 클래스의 정보를 알아야 합니다.여기서 Repository 구현체를 변경하면, 원래는 구현체를 몰라도 되는 Repository 사용 로직이 인스턴스 생성 로직과 같은 클래스 안에 담겨 있어 수정의 대상이 되었습니다.DI를 사용하여 수정된 설계에서는 객체들의 인스턴스를 생성하는 책임을 Config 클래스에 몰아줌으로써 Repository 구현체를 변경할 때에도 Service 코드는 영향을 받지 않고 Config 클래스만 수정하면 되도록 바뀌었습니다.

개발 · 프로그래밍 기타객체지향SOLIDDI

워밍업 클럽 4기 BE 클린코드 & 테스트 < Day4 미션 - 리팩토링, SOLID >

Day4 미션 - 1. 리팩토링 하기✔ 사용자가 생성한 '주문'이 유효한지를 검증하는 메서드.✔ Order는 주문 객체이고, 필요하다면 Order에 추가적인 메서드를 만들어도 된다. (Order 내부의 구현을 구체적으로 할 필요는 없다.)✔ 필요하다면 메서드를 추출할 수 있다.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; } 리팩토링한 코드public class Day4 { public boolean validateOrder(Order order) { try { if (order.notExistItem()) { throw new IllegalArgumentException("주문 항목이 없습니다."); return false; } if (order.incorrectTotalPrice()) { throw new IllegalArgumentException("올바르지 않은 총 가격입니다."); return false; } if (order.notExistCustomerInfo()) { throw new IllegalArgumentException("사용자 정보가 없습니다."); return false; } return true; } catch (IllegalArgumentException e) { log.error("주문 검증 중 예외 발생: {}", e.getMessage()); return false; } } } class Order { private List<Item> items; private Customer customer; public boolean notExistItem() { // 아이템 있는지 확인하는 로직 return true; // 예시로 true 반환 } public double totalPrice() { // 총 가격 계산 로직 return totalPrice; } public boolean incorrectTotalPrice() { return totalPrice() < 0; } public boolean notExistCustomerInfo() { // 사용자 정보가 있는지 확인하는 로직 return true; // 예시로 true 반환 } }중첩 조건문을 분해해서 Early Return을 적용했다.부정어 제거 + 객체 설계. 객체 설계는 섹션3의 내용은 아니지만 함께 적용했다. 부정어를 사용하는 대신 부정의 의미를 나타내는 메서드를 만들고, 해당 메서드는 Order 객체 내에서 처리했다.if문 조건문의 추상화 수준을 같게 설정했다.단순 로그만 찍는 코드에서 예외처리로 변경했다. Day4 미션 - 2. SOLID 나만의 언어로 정의하기SOLID란, 객체지향 설계에서 지켜줘야 할 5개의 소프트웨어 개발 원칙이다. 1. Single Responsibility Principle; 단일 책임 원칙하나의 객체에 하나의 책임만!객체가 갖는 책임이 많아질수록 다른 객체들과의 관계가 깊어지게 된다. 즉, 결합도(의존성)가 늘어난다.결합도(의존성)가 크면 해당 객체의 변경에 따른 영향도와 범위가 커지게 된다.따라서 객체 하나에는 하나의 책임만 부여하여 응집도는 높게, 결합도(의존성)는 낮게 설계해야 한다. 2. Open-Closed Principle; 개방-폐쇄 원칙확장에는 열려있고, 수정에는 닫혀있게.신규 기능이 추가되어야 할 때, 기존 코드의 변경 없이 시스템 확장이 가능하게 설계해야 한다. 3. Liskov Substitution Principle; 리스코프 치환 원칙상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.즉, 가업승계 하듯이 부모 클래스가 일하던 곳에 자식 클래스가 가더라도 문제가 없어야 한다.자식 클래스는 부모의 모든 책임을 준수하며, 부모 클래스를 변경하지 않아야 한다.만약 LSP를 위반하면, 오동작하거나 예상 밖의 예외가 발생하게 된다. 4. Interface Segregation Principle; 인터페이스 분리 원칙기능 단위로 인터페이스를 분리 설계할 것하나의 인터페이스에 추상 메서드들을 이것저것 구현한다면,그 인터페이스를 상속받은 클래스는 자신이 사용하지 않는 메서드마저 억지로 구현해야 하는 상황이 올 수 있다.이런 불필요한 의존성으로 인해 결합도가 높아지고, 특정 기능이 여러 클래스에 영향을 미치게 된다. 5. Dependency Inversion Principle; 의존 역전 원칙객체들이 서로 정보를 주고 받을 때 의존 관계가 형성되는데, 이 때 하나의 약속이 있다.나(객체)보다 추상화 례벨이 높은 상위 수준의 모듈과 통신해야 한다는 약속이다.의존 역전 원칙은 고수준 모듈과 저수준 모듈 모두 추상화된 인터페이스에 의존해야 한다는 원칙이다.이 원칙을 지키면 저수준 모듈(구현체)가 바뀌어도 고수준 모듈에는 영향이 없다는 장점이 있다.== 결합도가 낮아짐 == 유지보수가 쉬워짐 

백엔드백엔드SOLID객체지향클린코드

워밍업 클럽 4기 - 백엔드 Day 4

1. 코드 리팩토링As-Ispublic 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; } To-Bepublic boolean validateOrder(Order order) { if (doesNotHaveItems(order)) { log.info("주문 항목이 없습니다."); return false; } if (doesNotHaveTotalPrice(order)) { log.info("올바르지 않은 총 가격입니다."); return false; } if (doesNotHaveCustomerInfo(order)) { log.info("사용자 정보가 없습니다."); return false; } return true; } boolean doesNotHaveItems(Order order) { return (order.getItems().size() == 0)? true : false; } boolean doesNotHaveTotalPrice(Order order) { return !(order.getTotalPrice() > 0)? true : false; } boolean doesNotHaveCustomerInfo(Order order) { return !order.hasCustomerInfo()? true : false; } 2. SOLID에 대하여 자기만의 언어로 정리SRP : 클래스는 단일 책임을 가져야 한다 OCP : 새로운 기능 추가는 확장으로 처리한다 LSP : 자식 클래스로 부모 클래스를 대체 가능하다 ISP : 인터페이스도 가능한 작은 단위로 분리한다 DIP : 구체보다 추상에 의존하자 SOLID는 읽기 쉽고, 유지 보수가 쉽고, 기능 추가/변경을 쉽게 한다  

백엔드리팩토링SOLIDSRPOCPLSPISPDIP

세뇨르

워밍업 클럽 2기(클린코드, 테스트코드) 과제 (Day 4)

Readable Code: 읽기 좋은 코드를 작성하는 사고법(링크)코드 리팩토링기존 코드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; }리팩토링 코드public boolean validateOrder(Order order) { if (order == null) { log.info("주문 정보가 없습니다."); return false; } if (order.hasNoItems()) { log.info("주문 항목이 없습니다."); return false; } if (order.isInvalidTotalPrice()) { log.info("올바르지 않은 총 가격입니다."); return false; } if (order.hasNoCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } return true; }public class Order { private List<Item> items; private double totalPrice; private CustomrInfo customerInfo; public boolean hasNoItems() { return items == null || items.isEmpty(); } public boolean isInvalidTotalPrice() { return totalPrice <= 0; } public boolean hasNoCustomerInfo() { return customerInfo == null; } }SOLID 정리SOLIDSRPSingle Responsibility Principle (단일 책임 원칙)하나의 클래스는 단 한 가지의 변경 이유만을 가져야 한다.객체가 가진 공개 메서드, 필드, 상수 등은 해당 객체의 단일 책임에 의해서만 변경 되어야 함.관심사의 분리높은 응집도, 낮은 결합도OCPOpen-Closed Principle (개방-폐쇄 원칙)확장에는 열려 있고, 수정에는 닫혀 있어야 한다.기존 코드의 변경 없이, 시스템의 기능을 확장할 수 있어야 한다.추상화와 다형성을 활용해서 OCP를 지킬 수 있다.LSPLiskov Substitution Principle (리스코프 치환 원칙)상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.자식 클래스는 부모 클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야 한다.LSP를 위반하면, 상속 클래스를 사용할 때 오동작, 예상 밖의 예외가 발생하거나, 이를 방지하기 위한 불필요한 타입 체크가 동반될 수 있다.ISPInterface Segregation Principle (인터페이스 분리 원칙)클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.인터페이스를 잘게 쪼개야 함ISP를 위반하면, 불필요한 의존성으로 인해 결합도가 높아지고, 특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.DIPDependency Inversion Principle (의존성 역전 원칙)상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.의존성의 순방향 : 고수준 모듈이 저수준 모듈을 참조하는 것 의존성의 역방향 : 고수준, 저수준 모듈이 모두 추상화에 의존하는 것저수준 모듈이 변경되어도, 고수준 모듈에는 영향이 가지 않는다.

백엔드클린코드SOLID리팩토링

스프링 핵심원리 기본편(김영한) 1 - 객체지향 DIP와 스프링 DI, IoC

  객체는 객체와 끊임없이 상호작용한다. 그렇기에 유연한 변경이 가능해야한다. 예를 들어, 자동차라는 상위 클래스를 다양한 자동차 브랜드로 구현될 수 있고, 운전자가 변화해도 자동차는 영향을 받지 않는다. 사용자, 주문, 할인 등 여러 독립적인 특징을 가진 기능은 클래스로 분리하여 각 클래스에서만 수정 및 사용한다.   역할과 구현을 분리 - 인터페이스와 콘크리트 클래스 인터페이스는 안정적이게, 확장이 무한대로 가능하게 설계해야한다.   SOLID 객체지향 설계 원칙 1. SRP 단일책임원칙 - 변경이 용이한 단위적 책임인가2. OCP 개방폐쇄원칙 - 코드의 변경 없이 확장이 가능한가(조립만으로 변경)3. LSP 리스코프 치환 원칙 - 하위 클래스는 인터페이스(상위 클래스)를 위반하지 않아야한다4. ISP 인터페이스 분리 원칙 - 여러 개의 인터페이스를 통해 명확한 기능을 갖고 있고, 대체 가능성이 높은 환경을 구현할 것5. DIP 의존관계 역전 원칙 - 추상화에 의존할 것, 인터페이스(역할)가 중심이 되어야한다. 구현체에 의존하면 다형성을 잃는다(재활용성을 잃는다) 스프링 컨테이너에 객체 지향 적용 객체를 생성하는 역할과 객체를 실행하는 역할을 분리.의존은 인터페이스로 하고, 설정 파일을 통해 구체적인 구현체를 의존 주입구현체 변경 시 설정 파일만 변경하면 된다.(조립)=> 제어의 역전; 어떤 구현체를 사용할 것인지 AppConfig(Spring)가 결정한다. 동적인 인스턴스 의존관계    

객체지향javaSOLIDspringDIIoCDIP강의김영한

채널톡 아이콘