HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
[KDT] SpringBoot Part4/강의자료/2022 LIVE SECTION - 3기/
02. 객체지향

02. 객체지향

학습목표

객체지향 언어의 특징에 대해 학습한다.
  • 객체지향 언어에서 객체란 무엇인지 학습한다.
  • 객체지향 언어의 특징에 대해 이해한다.
    • 객체들의 “책임”에 대해 이해한다.
    • 객체들간의 “의존”에 대해 이해한다.
    • “캡슐화”를 통한 정보은닉에 대해 이해한다.
  • 객체지향의 설계과정에 대해 이해한다.
  • SOLID 원칙에 대해 이해한다.

객체지향

  • 소프트웨어는 데이터와 데이터를 조작하는 코드(프로시저)로 구성되어 있다.
  • 객체지향 언어 에서는 데이터 및 프로시저를 객체(Object) 라는 단위로 묶는다.
// 객체 public class Product { // 데이터 private String name; private long price private long quantity; // 프로시저 public void increaseQuntity() { this.quantity++; }; }
객치제향 프로그램은 객체들의 상호관계로 구성된다.
  • 객체는 자신만의 기능을 제공하며, 각 객체들은 서로 연결되어 메세지를 주고 받으며 소프트웨어를 구성한다.

객체

  • 객체는 데이터와 데이터를 조작하는 프로시저(메소드)로 구성된다.
  • 객체는 객체마다 자신만의 책임(Responsibility)이 있다.
  • 객체는 서로의 책임을 하며 다른 객체와 의존(Dependency)하며 소프트웨어를 구성한다.
‘읽기 객체’ , ‘집계 객체’ , ‘정산 객체’는 각자의 책임을 가지며 서로 메시지를 주고 받으며 의존 하고 있다.

객체 책임

  • 객체가 가지는 책임을 결정하는 것이 객체 지향 설계의 출발점이다.
  • 매출 데이터 정산에 대한 기능 목록 정리
    • 업주의 매출 데이터를 읽는다.
    • 매출 데이터를 업주별로 집계한다.
    • 매출 데이터를 업주에게 정산한다.
    • 전체 흐름을 제어한다.
    • // 전체 흐름제어 public class SalesFlowService { private SalesInfoRepository salesInfoRepository; private SettleRepsitory settleRepository; public void calculateSalesData(LocalDate txDate) { //1. 업주의 매출 데이터를 읽어온다. List<SalesInfo> salesInfos = salesInfoRepository.findByTxDate(txDate); // 2. 매출데이터를 업주별로 집계한다. Map<Long, List<SalesInfo>> salesInfoByShopNumber = salesInfos.stream().collect(groupingBy(SalesInfo::shopNumber)); // 3. 매출데이터를 업주에게 정산한다. for (String shopNumber : salesInfoByShopNumber.keySet()) { List<SalesInfo> specificShopSalesInfos = salesInfoByShopNumber.get(shopNumber); List<SettleDate> specificShopSettleDatas = specificShopSalesInfos.stream().map(SettleDate::convert).collect(toList()); settleRepository.save(specificShopSettleDatas); } } }
      SalesFlowService 는 3가지 책임을 가진다.
    • 각 책임을 아래와 같이 분리할 수 있다.
    • public class SalesFlowService { private SalesInfoReader salesInfoReader; private SalesInfoCalculator salesInfoCaculator; private SettleDataWriter settleDataWriter; public void calculateSalesData(LocalDate txDate) { // 1. 업주의 매출 데이터를 읽어온다. List<SalesInfo> salesInfos = salesInfoReader.findByTxDate(txDate); // 2. 매출데이터를 업주별로 집계한다. Map<Long, List<SalesInfo>> salesInfoByShopNumber = salesInfoCaculator.calculate(salesInfos); // 3. 매출데이터를 업주에게 정산한다. settleDataWriter.write(salesInfoByShopNumber); }
      각 객체는 하나의 책임을 가진다.
    • 객체가 갖는 책임의 크기는 작을수록 좋다.
    • 하나의 객체가 많은 기능을 포함하면, 그 기능관 관련된 데이터들도 한객체에 포함된다.
    • 객체의 책임이 커질수로 유지보수가 어려워진다.

객체 의존

  • 객체지향 프로그램에서는, 객체가 다른 객체의 기능을 이용해서 자신의 기능을 완성 한다.
public class SalesFlowService { private SalesInfoReader salesInfoReader; private SalesInfoCalculator salesInfoCaculator; private SettleDataWriter settleDataWriter; public void calculateSalesData(LocalDate txDate) { // 1. 업주의 매출 데이터를 읽어온다. List<SalesInfo> salesInfos = salesInfoReader.findByTxDate(txDate); // 2. 매출데이터를 업주별로 집계한다. Map<Long, List<SalesInfo>> salesInfoByShopNumber = salesInfoCaculator.calculate(salesInfos); // 3. 매출데이터를 업주에게 정산한다. settleDataWriter.write(salesInfoByShopNumber); }
흐름 제어 객체는 매출 데이터 읽기, 집계, 정산 객체의 기능을 이용하여 자신의 기능을 완성했다.
  • 객체에서 다른 객체를 생성 하거나 다른 객체의 메서드를 호출할 때, 해당 객체에 의존 한다고 한다.
위 코드에서 SalesFlowService는 SalesInfoReader, SalesInfoCalculator, SettleDataWriter 에 의존한다.
  • 의존성 전이
    • 객체의 변화로 인해 다른 객체에 영향이 가게 된다.
    • 변경이 많은 객체는 의존이 많이 되지 않도록 해야 한다.
    • 변경은 의존 관계를 따라 전이 된다.
public class Restaruant { private Customer customer; private Food food; public void sellFood() { customer.buyFood(food); } } public class Customer { private long fund; private Food food; public long buyFood(Food food) { fund = fund - food.caculateAmount(); food = food; } } public class Food { private long price; private long quantity; public long calculateAmount() { return price * quantity; } }
Food 의 변화는 Customer 에게 영향을 미치며 Customer의 변화는 Restaruant 에 변화를 미친다.

캡슐화

  • 객체가 내부적으로 기능을 어떻게 구현 했는지 감추는 것을 말한다.
  • 캡슐화를 통해 내부구현의 변경이 그 기능을 사용하는 코드에 영향을 받지 않게한다.
public class Bank { public void getFirstDigitOfAfterRegistrationNumber(Customer customer) { if(customer.isMale) { return 1; } return 2; } } public class Company { public void getFirstDigitOfAfterRegistrationNumber(Customer customer) { if(customer.isMale) { return 1; } return 2; } } public class Customer { private LocalDate birthDate; private boolean male; public boolean isMale() { return male; } public LocalDate getBirthDate() { return birthDate; } }
캡슐화가 되지 않은 코드는 내부구현이 외부로 노출된다.
  • 주민등록번호 첫째자리를 구하는 로직이 2000년대생 이후로 남자는 ‘3’ 여자는 ‘4’로 변경 되었다면 Customer 의 데이터를 사용하는 Bank 와 Company 코드는 코드 수정이 발생한다.
public class Bank { public int getFirstDigitOfAfterRegistrationNumber(Customer customer) { return customer.getFirstDigitOfAfterRegistrationNumber(); } } public class Company { public int getFirstDigitOfAfterRegistrationNumber(Customer customer) { return customer.getFirstDigitOfAfterRegistrationNumber(); } } public class Customer { private LocalDate birthDate; private boolean male; public int getFirstDigitOfAfterRegistrationNumber(Customer customer) { if (birthDate.isAfter(LocalDate.of(2000,1, 1)) { return after2000FirstDigitOfAfterRegistrationNumber(); } else { return before2000FirstDigitOfAfterRegistrationNumber(); } } private int after2000FirstDigitOfAfterRegistrationNumber() { if(customer.isMale) { return 3; } return 4; } private int before2000FirstDigitOfAfterRegistrationNumber() { if(customer.isMale) { return 1; } return 2; } }
캡슐화된 코드는 내부구현을 감춤으로써 변경을 외부로 전파하지 않는다.
  • 캡슐화를 통해서 Customer의 구현이 변경되어도 이를 사용하는 외부 구현에는 영향을 미치지 않는다.

객체지향 설계 과정

  1. 소프트웨어 구현에 필요한 기능을 찾고 이를 세분화한다.
  1. 세분화된 기능을 알맞은 객체에 할당한다.
  1. 객체간에 어떻게 메시지를 주고받을 지 결정한다.
  1. 2, 3번 과정을 지속적으로 반복한다.

SOLID

  • 단일 책임 원칙 (SRP)
  • 개방-폐쇄 원칙 (OCP)
  • 리스코프 치환 원칙 (LSP)
  • 인터페이스 분리 원칙 (ISP)
  • 의존 역전 원칙 (DIP)
 

단일 책임 원칙

Single responsibility principle; SRP
  • 객체가 가지는 책임을 결정하는 것이 객체 지향 설계의 출발점이다.
  • SRP 는 책임과 관련된 “객체는 단 한 개의 책임을 가져야 한다.” 는 원칙이다.
    • 객체가 여러 책임을 가지게 되면, 각 책임 마다 변경되는 이유가 발생한다.
    • 객체의 변경은 의존된 다른 객체에 변화를 유발한다.
    • 따라서, 객체는 단 한개(최대한 적은)의 책임을 가져야 한다.
  • 위에서 살펴본 SalesFlowService 를 통해 SRP 를 살펴본다.
    • // 전체 흐름제어 public class SalesFlowService { private SalesInfoRepository salesInfoRepository; private SettleRepsitory settleRepository; // 책임 1 public void calculateSalesData(LocalDate txDate) { // 책임 2 List<SalesInfo> salesInfos = salesInfoRepository.findByTxDate(txDate); // 책임 3 Map<Long, List<SalesInfo>> salesInfoByShopNumber = salesInfos.stream().collect(groupingBy(SalesInfo::shopNumber)); // 책임 4 for (String shopNumber : salesInfoByShopNumber.keySet()) { List<SalesInfo> specificShopSalesInfos = salesInfoByShopNumber.get(shopNumber); List<SettleDate> specificShopSettleDatas = specificShopSalesInfos.stream().map(SettleDate::convert).collect(toList()); settleRepository.save(specificShopSettleDatas); } } }
      SalesFlowService 4가지 책임에 대해 변경이 발생 가능하다.
      public class SalesFlowService { private SalesInfoReader salesInfoReader; private SalesInfoCalculator salesInfoCaculator; private SettleDataWriter settleDataWriter; // 책임 1 public void calculateSalesData(LocalDate txDate) { // 책임을 다른객체에 위임 List<SalesInfo> salesInfos = salesInfoReader.findByTxDate(txDate); Map<Long, List<SalesInfo>> salesInfoByShopNumber = salesInfoCaculator.calculate(salesInfos); settleDataWriter.write(salesInfoByShopNumber); }
      SRP를 적용한 SalesFlowService는 오직 한가지 책임만 가진다.
 

개방 폐쇄 원칙

Open-close principle; OCP
  • 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
    • 기능을 변경하거나 확장할 수 있으며, 그 기능을 사용하는 코드는 수정하지 않는다.
    • 즉, 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
public class Shop { private Food food; private long salesAmount; private Food sell() { this.salesAmount += food.calculateAmount(); return food; } }
음식 판매 가게의 코드는 위와 같다.
public class Shop { private Car car; private long salesAmount; private Car sell() { this.salesAmount += car.calculateAmount(); return car; } }
판매할 품목을 ‘음식’ → ‘자동차’로 변경이 되었다.
  • 위 예제코드에서 기능의 변경으로 그 기능을 사용하는 코드가 변경되었다.
  • 추상화를 통해 아래와 같이 리펙토링이 가능하다.
public class Shop { private Product product; private long salesAmount; private Product sell() { this.salesAmount += car.calculateAmount(); return product; } } public interface Product { public long calculateAmount(); } public class Food implements Product { @Override public long calculateAmount() { // 음식판매금액 구현 }; } public class Car implements Product { @Override public long calculateAmount() { // 자동차판매금액 구현 }; }
  • OCP 를 적용한 코드는 기능의 변경과 확장(음식점의 판매 품목 변경)에는 열려 있으며, 그 기능을 사용하는 코드의 변화(판매 품목이 변경되어도 가게 객체의 변화는 없다.)에는 닫혀있다.
 

리스코프 치환 원칙

Liskov substitution principle; LSP
  • 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.
public int calculate(Item item) { return item.calculate(); }
상위 타입인 Item이 있고, 그 하위 타입이 Apple이라면, 메소드 파라미터로 Item이 아닌 Apple을 넘겨도 코드가 동작해야 한다.
  • LSP가 제대로 지켜지지 않으면, 다형성에 기반한 OCP 역시 위반되게 된다.
  • 직사각형 정사각형 예제
    • 직사각형 ≠ 정사각형
    • 정사각형 = 직사각형
// 직사각형 객체 public class Rectangle { private int width; private int height; public void setWidth(final int width) { this.width = width; } public void setHeight(final int height) { this.height = height; } public int getWidth() { return width; } public int getHeight() { return height; } } // 정사각형은 직사각형이기에 직사각형 객체를 상속받아 정사각형 객체를 정의 public class Square extends Rectangle { // 정사각형은 가로,세로가 동일하기에 직사각형의 setWidth, setHeight 기능을 재정의한다. @Override public void setWidth(final int width) { super.setWidth(width); super.setHeight(width); } @Override public void setHeight(final int height) { super.setWidth(height); super.setHeight(height); } } public class Application() { public void increaseHeight(Rectangle rectangle) { if (rectangle.getHeight() <= rectangle.getWidth()) { rectangle.setHeight(rectangle.getWidth() + 1); } } }
  • increaseHeight 메소드는 직사각형의 가로(width)와 세로(height)를 비교하여, 가로의 길이에 1을 더한 만큼의 세로길이를 갖게 만드는 역할을 한다.
  • 위 메소드는 정사각형에서는 정상동작 하지 않음으로 코드를 아래와 같이 수정해야 한다.
public class Application() { public void increaseHeight(Rectangle rectangle) { // 정사각형일 때는 수행하지 않는다. if (rectangle instanceof Square) { throw new IllegalStateException(); } if (rectangle.getHeight() <= rectangle.getWidth()) { rectangle.setHeight(rectangle.getWidth() + 1); } } }
위 코드는 OCP 원칙이 위배된다.
  • increaseHeight 메소드는 특정 객체에서는 예외가 발생하므로 확장에는 닫혀있다.
  • 리스코프 치환 원칙을 지키지 않으면 개방 폐쇄 원칙을 위반하게 된다. 따라서 상속 관계를 잘 정의하여 LSP 원칙이 위배되지 않도록 설계해야 한다.

인터페이스 분리 원칙

Interface segregation principle; ISP
  • 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다.
왼쪽 다이어그램에서는 Client는 각각 function1, function2 만을 사용한다. 우측 다이어그램과 같이 각 Client가 사용하는 메소드만을 의존하도록 한다.
  • 클라이언트를 기준으로 인터페이스를 분리함으로써, 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라고 영향을 받지 않도록 하는것이 ISP의 핵심이다.

의존성 역전 원칙

Dependency inversion principle; DIP
  • 의존 관계를 맺을 때, 변하기 쉬운것 (구체적인 것) 보다는 변하기 어려운 것(추상적이 것)에 의존해야 한다.
  • 즉, 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.
    • 고수준 모듈 : 변경이 없는 추상화된 클래스 (interface, abstract class)
    • 저수준 모듈 : 변하기 쉬운 구체 클래스 (class)
    •  
       

SOLID 정리

  • SRP 와 ISP 는 객체가 커지는 것을 막아준다.
    • 객체가 단일 책임을 갖도록 하고(SRP), 클라이언트 마다 특화된 인터페이스를 구현(ISP)하게 함으로써 한 기능의 변경이 다른 곳까지 미치는 영향을 최소화 한다.
  • LSP와 DIP는 OCP를 서포트한다.
    • OCP는 자주 변화되는 부분을 추상화(DIP)하고 다형성을 이용함(LSP)으로써 기능확장에는 용이하되 기존 코드의 변화에는 보수적이 되도록 만들어 준다.
    •