Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_19일차_''객체지향원칙 OOP"

LEFT 2024. 12. 27. 16:46

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [19]일차

🚀19차에는 객체지향원칙과 객체지향설계원칙에 대해 배울 수 있었다.

객체지향원칙 핵심개념으로 정보처리기사 공부할때 외워놨던 "캡상추다"를 다시 접할 수 있었다.

캡슐화, 상속, 추상화, 다형성은 회고 초반에 진행했던 실습들과 연관이 많이 되어있어서 개념의 이해가 더 쉬웠다.

객체지향설계원칙으로는 SOLID 가 있는데 이또한 이전에 공부할때는 약어만 외웠었지만 실제로 어떤 일들을 하는지를 

예제코드와 함께 공부해보니 왜 사용하는 것인지, 어느때 사용해야하는지를 익힐 수 있었다.


객체지향원칙 OOP

핵심개념

  • 캡상추다 ➡️ 캡슐화 / 상속 / 추상화 / 다형성

추상화

  • 복잡한 시스템이나 객체를 “단순화 하여 핵심적인 속성이나 기능만 나타냄”
  • 구체적인 세부구현은 감추고, 필요한 “인터페이스나 기능만 노출”
  • Payment 인터페이스 정의 ➡️CreditCard, PayPal 등처럼 결제방법을 오버라이딩 구현하여 “구체성을 숨김”

캡슐화

  • 객체의 속성과 메소드를 하나의 논리적 단위로 묶음
    (ex. computer 클래스 안
    keyboard() 메소드, monitor 메소드, private boolean togglePower 필드, private String userName 필드 등)

  • 객체의 속성을 외부에서는 이 객체가 제공하는 메소드를 통해서만 접근하도록함

  • 내부 구현을 감추고(정보 은닉), 필요한 인터페이스만 노출
    ex. User 클래스 private int password 를 getPassword()를 통해서만 접근가능, setPassword() 는 암호화 처리담당

상속

  • 상위 부모 클래스의 속성과 메소드를 하위 자식 클래스에서 물려받아 사용
  • 필요부분 오버라이딩 및 확장 가능

다형성

  • 같은 메소드 호출이라도 객체의 실제 타입에 따라 “다른 동작” 수행
  • 오버라이딩, 오버로딩 등을 통해 다양한 형태로 동작
  • ex. Animal타입으로 speak() 호출 → Dog객체면 “멍멍”, Cat 객체면 “야옹”

❓설계원칙이 중요한 이유

➡️코드의 유지보수성 높이기 (각 객체의 책임이 분명, 수정 범위가 최소화)
➡️확장성과 유연성 확보 (변화가능한 지점을 미리 추상화하여 기능추가로 인한 코드 수정 최소화)
➡️의존성과 결합도 줄이기 (재사용성 향상, 모듈간 결합도 낮추고 응집도 높이기)


객체지향설계원칙 5가지

SOLID

  • SRP / OCP / LSP / ISP / DIP

SRP

  • Single Responsibility Principle
  • 클래스나 모듈은 오직 하나의 책임만 가지기
  • 여러 역할 수행으로 인한 변경범위 증가와 결합도가 높아짐을 방지
  • 별도의 클래스로 분리” ➡️오직 하나의 책임만 가질 수 있게되어 결합도가 높아짐을 방지
  • 하나의 클래스에는 하나의 책임만 가질 수 있도록 설계
// User 클래스
// 1. SRP원칙 적용 하기 전
public void setPassword(String password){
    if(password != null && password.length() >= MAX_PW_LENGTH){
        this.password = password;
    }else{
        throw new IllegalArgumentException("유효하지 않은 비밀번호입니다!");
    }
}
// User클래스
// 2. SRP원칙 적용한 후
public void setPasswordValidator(String password){
    if(PasswordValidator.isValid(password)){
        this.password = password;
    }
    else{
        throw new IllegalArgumentException("유효하지 않은 비밀번호입니다!");
    }
}

// Password 검증기 클래스 분리
class PasswordValidator{
    public static boolean isValid(String password){
        return password != null && password.length() >= 6;
    }
}
  • PasswordValidator와 같은 클래스로 별도로 분리하여 메소드를 호출하는 방식으로 사용

 

🚀실습 - SRP Game 만들기

// GameSystem클래스
public static void printUserInfo(String name, int level){
        // ... 정보출력 파트... 
        
        try(FileWriter writer = new FileWriter("src/sample/SRPGameUser.txt")){
            writer.write("유저 : " + name);
            writer.write("레벨 : " + level);
        }catch(Exception e){
            System.out.println(e + "올바르지 않은 유저와 레벨입니다.");
        }
        System.exit(0);
    }
  • GameSystem 클래스 : 유저가 접속을 종료하면 유저가 플레이한 정보를 SRPGameUser.txt 파일에 쓸 수 있도록 구현
// EventManager 클래스
public static int setEventMonth(){
        gs.setMonth(12);
        return gs.getMonth();
    }
    public static void printAllEvent(){
        int month = setEventMonth();
        System.out.println("=====[" + month + "]월의 이벤트=====");
        // ...이벤트 정보 출력 파트...
    }
  • EventManager 클래스 : GameSystem클래스에서 Month정보를 관리하므로 Month정보를 Setter로 갱신
  • printAllEvent() : 갱신한 Month정보로 이벤트 목록을 출력
// 유저 클래스
// 유저 접속 종료
public void userExit(){
    SRPGameSystem.printUserInfo(name, level);
}

// 현재 진행중인 이벤트 확인하기
public void checkEvent(){
    SRPEventManager.printAllEvent();
}
  • 게임을 진행하는 유저가 (접속 종료) 및 (이벤트 확인)을 호출할 권한이 있으므로
    각 클래스의 메소드를 유저에서 호출할 수 있도록 구현

실행결과
파일 저장 확인


OCP

  • Open-Closed Principle
  • 확장에는 열려있고, 수정에는 닫혀있기
  • 새 기능 추가시 기존 코드를 크게 수정하지 않음

🚀실습 - 축구선수 출력 : OCP

interface FootballLeagues{
    void playerInfo(String name, String team);
}

class PremierLeague implements FootballLeagues{
    private String leagueName;

    public PremierLeague(String leagueName) {
        this.leagueName = leagueName;
    }
    @Override
    public void playerInfo(String name, String team) {
        System.out.println(leagueName + " [" + team + "]의 - " + name);
    }
}
FootballLeagues epl = new PremierLeague("PremierLeague");
FootballLeagues spain = new Laliga("Laliga");

epl.playerInfo("Van den berg", "Brentford");
spain.playerInfo("Aspas", "Celta de Vigo");

  • 다른 리그를 추가하더라도 FootballLeagues를 구현하는 방식으로 기존 코드를 변경하지 않고 추가가 가능
    ➡️확장에는 열려있고 수정에는 닫혀있음

 

🚀실습 - 축구선수 출력 : OCP (리팩토링)

  • 인터페이스를 구현하는 “연결”클래스를 만든다.
  • 연결클래스를 main메소드에서 객체로 생성하여 new생성자를 통해 인자를 넣어
    리그 별로 데이터를 불러올 수 있도록 리팩토링
// 연결 클래스 추가
class LeaguesType{
    public void process(FootballLeagues football, String name, String team){
        football.playerInfo(name, team);
    }
}
LeaguesType league = new LeaguesType();
league.process(new PremierLeague("PremierLeague"), "Baines", "Everton FC");
league.process(new Laliga("Laliga"), "Ferran", "FC Barcelona");

  • 연결클래스의 객체를 생성하여, new생성자로 인자들을 넣은 후 출력 확인

LSP

  • Liskov Substitution Principle
  • 자식클래스는 부모클래스의 행위를 깨뜨리지 않고 대체가능해야함
  • 상속구조에서의 다형성을 보장

▶️실습 - 직사각형과 정사각형 (리스코프 치환 전)

class Square extends Rectangle{
	public Square(int side){
		super(side, side);
	}
}
  • Rectangle 클래스 : 직사각형 클래스로 필드 width, height는 Getter, Setter메소드가 있다.
  • Square 클래스 : 정사각형 클래스로 Rectangle클래스를 상속받고
    super로 보낼때 (side, side)로 인자를 보낼 수 있을 것이다.

  • 정사각형.setWidth()으로 값을 변경할때 정사각형이기때문에 두 변(side) 다 바뀌어야하는데,
    width값만 바뀌어서 예상한 결과가 나오지 않는다 ➡️리스코프 치환이 되지 않은 경우이다.

▶️실습 - 직사각형과 정사각형 (리스코프 치환 후)

@Override
public void setHeight(int height) {
    super.height = height;
    super.width = height;
}

@Override
public void setWidth(int width) {
    super.height = width;
    super.width = width;
}
  • square클래스의 Setter메소드에서 입력값(height or width)에 따라 들어온 값으로
    두 변(side) 모두 갱신할 수 있도록 수정

▶️실습 - 직사각형과 정사각형 (리팩토링 - 인터페이스 사용)

  • 직사각형과 정사각형 클래스가 Shape라는 인터페이스를 구현하도록 함
interface Shape{
    int area();
}

// ... Rectangle 클래스 파트

class Square2 implements Shape{
    private int side;

    public Square2(int side) {
        this.side = side;
    }

    public void setSide(int side) {
        this.side = side;
    }
    @Override
    public int area() {
        return side * side;
    }
}

//Main메소드
Shape square = new Square2(5);
System.out.println("정사각형 넓이 : " + square.area());
  • 인터페이스의 area() : 넓이 구하기 메소드를 이용하여 그 메소드를 인터페이스를 구현한 클래스에서
    오버라이딩하여 사용할 수 있게 만드는 것이 상속으로 만드는 것보다 좋은 설계일 수 있다.

▶️실습 - 자동차 주행 프로그램 (추상클래스 사용)

  • 공통된 부분을 추상클래스 상위 객체로 만든다.
  • 이 추상클래스를 상속받아서 사용하도록 하면 메소드를 강제할 수 있고
    그 메소드는 오버라이딩하여 구현부를 달리할 수 있을 것

  • 인터페이스 추상메소드만 가질 수 있지만
    추상클래스추상메소드 외 다른 것들도 가질 수 있으므로 추상클래스를 상속받아서 구현
abstract class Vehicle{
    abstract void drive();
}

// ... Car클래스 구현 파트
  • 추상클래스로 drive() 추상메소드를 선언
class ElectricCar extends Vehicle{
    private int battery;
    public ElectricCar(int battery) {
        this.battery = battery;
    }
    @Override
    public void drive() {
        while(battery > 0){
            battery--;
            System.out.println("주행 중... 남은 배터리량 : " + battery);
        }
        System.out.println("배터리가 없어 주행이 불가능합니다.");
    }
}
  • Car클래스의 drive()는 if문으로 오버라이딩해보고, ElectricCar클래스의 drive()는 while문으로 오버라이딩
  • 생성자로 배터리 값을 받고, 그 배터리를 필드 battery에 갱신시켜서 필드 battery로 출력문을 수행


ISP

  • Interface Segration Principle
  • 클라이언트는 자신이 사용하지 않는 메소드에 의존하지 않아야함
  • 인터페이스 세분화로 “불필요한 의존 줄이기”

▶️실습 - 주문 시스템 : ISP (적용 전)

interface OrderService{
    void placeOrder(String item);
    void cancelOrder(String orderId);
}

class OrderClient{
    private final OrderService orderService;

    public OrderClient(OrderService orderService) {
        this.orderService = orderService;
    }

    public void createNewOrder(){
        orderService.placeOrder("book");
    }
}
  • 인터페이스 안에 두개의 기능을 가진 메소드가 있다.
  • 하지만 인터페이스가 바뀌었을때 이 인터페이스를 의존하고 있는 클래스들은
    인터페이스의 수정으로 인해 사용하지 않는 인터페이스의 메소드가 있더라도 영향을 받을 수 있다.

  • 주로 인터페이스의 설계가 모호하거나 너무 많은 기능을 가지고 있었을때 발생하는 경우이다. 

  • 따라서 인터페이스 안에 A기능과 B기능을 같이 정의하는 것이 아닌
    각각의 인터페이스로 정의해두는 것으로 해결할 수 있는데 이를 “인터페이스 분리 원칙” ISP 라고 한다.

  • 하나의 인터페이스가 너무 많은 기능을 가지고 있는 것 = ISP(인터페이스분리원칙)
    이는 하나의 클래스가 한 책임만 지도록해야한다는 SRP (단일책임원칙)과 유사하다.

▶️실습 - 주문 시스템 : ISP (적용 후)

interface OrderOperations{
    void placeOrder(String item);
}
interface CancelOperations{
    void cancelOrder(String orderId);
}
  • 인터페이스를 분리
class OnlineOrderService implements CancelOperations, OrderOperations{
    @Override
    public void cancelOrder(String orderId) {
        System.out.print("[주문취소번호 : " + orderId + "] - ");
        System.out.println("주문이 취소되었습니다.");
    }

    @Override
    public void placeOrder(String item) {
        System.out.println("[" + item + "] 주문완료!");
    }
}
public static void main(String[] args) {
  // 두가지 인터페이스를 구현한 서비스 클래스를 선언
  OnlineOrderService service = new OnlineOrderService();

  // service는 두가지 인터페이스를 포함하므로 그 중 OrderOperations타입이 인자로 전달된다.
  OrderClient client = new OrderClient(service);

  client.createNewOrder();
  service.cancelOrder("NO241227");
}
  • OnlineOrderService 와 OrderClient를 생성 후 OrderClient의 매개변수로 OnlineOrderService의 객체를 넣어주었다.
  • OnlineOrderService는 두 가지 인터페이스 (OrderOperations, CancelOperations)를 포함하고 있으므로
  • OrderClient가 생성자로 가지는 타입인 OrderOperations를 받아서 메소드를 수행할 것이다.


DIP

  • Dependency Inversion Principle
  • 상위 추상 모듈은 하위 구현 모듈에 의존하지 않도록함
  • 추상화에 의존하여 변경에 유연하게 대응

💡각 원칙 별로 실습을 진행하면서 원칙들이 조금씩 다르면서도 조금은 유사한 부분도 발견할 수 있었다.

예제 이외에도 비슷한 상황의 샘플데이터가 없을까 찾아보다가 아이디어가 생기는대로 실습을 진행해볼 수도 있었다.

오늘 회고에서 확장에는 열려있어야하고 수정에는 닫혀있어야한다는 OCP 원칙이 가장 기억에 남는다.

LSP원칙과 ISP원칙의 경우에는 원칙을 고려하여 설계하면서 더 복잡해진 것 같다는 느낌이 들기도했지만

LSP원칙에서 정사각형과 직사각형의 예외가 발생하는 상황을 보니 LSP원칙이 객체지향설계원칙이 왜 중요한지를 알 수 있었다.🚀