Cotato 10th 백엔드 네트워킹 회고 1

1주차 과제는 과제의 요구 사항를 충족하면서 기존의 코드를 객체지향적인 코드로 변경하는 과제였다. 과제를 수행하면서 고민해볼 주요 사항들은 다음과 같았다.

 

  • 상수 관리
  • 코드 중복의 없애는 것
  • 가독성이 좋은 코드
  • 읽기 쉬운 변수명, 메서드명

 

구현 로직

 

추상 클래스

 

각 Item 별로 남은 판매 일수, Quality 등을 업데이트하는 기능이 일부 공통적으로 수행되기 때문에 중복된 코드를 없애기 위하여 추상 클래스로 공통된 부분은 일반 메소드를 통해 구현하고, 각 item 별로 맥락에 따라 구현되어야 하는 부분은 추상 메소드로 정의하였다.

 

대표적인 예로 update() 를 추상 메소드로 두고 각 아이템 별로 해당 메소드를 오버라이딩 하는 방식으로 중복 코드를 없애주었다.

 

public abstract class GildedRoseItem {

    protected Item gildedRoseItem;

    public GildedRoseItem(Item item) {
        this.gildedRoseItem = item;
    }

    public abstract void update();

    public abstract void init();

		...

}
public class Normal extends GildedRoseItem {

    public Normal(Item item) {
        super(item);
    }

    @Override
    public void update() {

        decreaseQuality(1);
        decreaseSellIn();

        if (gildedRoseItem.sellIn < 0) // 판매 일수가 0보다 작은 경우 품질 2배 감소
            decreaseQuality(1);
    }
		...
}

 

각 item 별로 맥락에 따라 다르게 오버라이딩하여 구현할 수 있는 인터페이스를 정의하여 구현하는 방법도 생각하였지만 다음과 같은 추상 클래스와 인터페이스의 대표적인 차이로 인하여 인터페이스를 통하여 구현하지는 않았다.

 

  • 추상 클래스는 추상 메서드와 일반 메서드를 포함할 수 있다는 특성이 있기 때문에 Quality를 증가시키거나 감소시키는 공통적으로 수행되는 로직들은 추상 클래스의 일반 메소드에 포함시켜서 하위 클래스에서 다시 구현되는 일이 없도록 하였다.

 

  • 인터페이스를 통하여 구현하면 공통적으로 수행되는 로직들을 다시 Overriding하는 문제가 발생하게 된다.

 

추상 클래스

  • 다른 클래스들이 상속받아야 하는 공통적인 기능과 상태를 정의하는 데 사용됩니다. 한 클래스가 단 하나의 추상 클래스만 상속받을 수 있습니다.
  • 추상 메서드와 일반 메서드(구현된 메서드) 모두를 포함할 수 있습니다. 이 때문에 추상 클래스는 공통 기능의 기본 구현을 제공할 수 있습니다.

인터페이스

  • 클래스가 구현해야 할 메서드의 목록을 정의하며 다중 구현이 가능합니다. 즉, 한 클래스가 여러 인터페이스를 구현할 수 있습니다.
  • 기본적으로 추상 메서드만 포함했으나, Java 8 이후부터는 default 메서드와 static 메서드를 포함할 수 있습니다. 하지만 일반적인 필드를 가질 수 없으며, 상수(static final 필드)만 허용됩니다.

 

코드 리팩토링

 

동아리원들이 해준 코드 리뷰와 과제를 시작하면서 해볼 고민들을 바탕으로 내 코드에서 부족한 점을 파악하고 수정하는 과정을 진행했다.

 

상수 관리

 

기존 코드

public void update() {

    if (gildedRoseItem.sellIn <= 0) {
        resetQuality();
    } else {
        increaseQuality(1);
        if (gildedRoseItem.sellIn < 11)
            increaseQuality(1);

        if (gildedRoseItem.sellIn < 6)
            increaseQuality(1);
    }
    decreaseSellIn();
}

 

내 기존의 코드를 보게 되면 6, 11, 0 … 들의 상수가 남발되고 있었다. 기존 코드를 작성할 때는 아무 생각이 없었지만 다시 생각해보니 나중에 수정하는 일이 생긴다면 일일이 다 바꿔야 하니 너무 귀찮을 거 같다는 생각이 들었다…

 

개선 코드

public abstract class GildedRoseItem {

		... 

    // 품질 관련 상수
    protected static final int MAX_QUALITY = 50; // 최대 품질
    protected static final int MIN_QUALITY = 0; // 최소 품질
    protected static final int RESET_QUALITY = 0; // 품질 초기화 값
    protected static final int COMMON_QUALITY_DECREASE_RATE = 1; // 품질 감소율
    protected static final int COMMON_QUALITY_INCREASE_RATE = 1; // 품질 증가율

    // 판매 일수 관련 상수
    protected static final int SELL_IN_DECREASE_RATE = 1; // 판매 일수 감소율

    ... 

}
public class Normal extends GildedRoseItem {

		... 

    @Override
    public void update() {

        decreaseQuality(COMMON_QUALITY_DECREASE_RATE);
        decreaseSellIn();

        if (gildedRoseItem.sellIn < 0) {
            decreaseQuality(COMMON_QUALITY_DECREASE_RATE);
        }
    }
		
		... 
}
public class BackstagePasses extends GildedRoseItem {

    private static final int BACKSTAGE_SELL_IN_THRESHOLD_10 = 10; // 판매 일수가 10일 이하인 경우
    private static final int BACKSTAGE_SELL_IN_THRESHOLD_5 = 5; // 판매 일수가 5일 이하인 경우

		... 

    @Override
    public void update() {

        if (gildedRoseItem.sellIn <= 0) {
            resetQuality();
        } else {
            increaseQuality(COMMON_QUALITY_INCREASE_RATE);

            if (gildedRoseItem.sellIn < BACKSTAGE_SELL_IN_THRESHOLD_10) {
                increaseQuality(COMMON_QUALITY_INCREASE_RATE);
            }

            if (gildedRoseItem.sellIn < BACKSTAGE_SELL_IN_THRESHOLD_5) {
                increaseQuality(COMMON_QUALITY_INCREASE_RATE);
            }
        }
        decreaseSellIn();
    }
		
		... 
}

 

하드 코딩된 숫자를 의미 있는 이름을 가진 상수(Magic Number)로 변경하여 관리하도록 하였다. 이렇게 개선을 해보니 각 코드의 가독성이 높아졌고, 유지보수의 상황이 생겼을 때 상수 선언문만 바꿔주면 되니 효율성이 높아졌다.

 

또한 공통된 상수를 추상 클래스에 정의함으로써, 하위 클래스에서 공통된 로직을 처리할 때 재사용할 수 있어서 중복된 코드를 줄이고 간결하게 구현할 수 있었다.

 

이러한 개선을 통해서 가독성, 의미의 명확성, 유지보수성, 재사용성이 향상되었다. 다음부터는 하드 코딩된 수를 남용하는 것이 아니라 재사용성을 고려해 매직 넘버를 관리하도록 해야겠다.

 

가독성이 좋은 코드, 변수명, 메소드명

 

기존 코드

protected void increaseQuality(int quality) {
    if (gildedRoseItem.quality < 50) {
        gildedRoseItem.quality = Math.min(gildedRoseItem.quality + quality, 50);
    }
}
...

 

내 코드에서 동아리원들이 리뷰를 달아준 내용을 보니 가독성에 대한 리뷰도 존재하였다.

 

코드를 작성하고 리팩토링을 하기 위해 오랜만에 코드를 보니 저 함수가 어떤 기능을 하는 함수인지 명확하지 않았고, 변수명을 통해 빠른 판단이 불가하였다.

 

또한 주석을 달아서 읽기 쉬운 부분도 있었지만, 주석이 띄엄띄엄 써져 있어 빠르게 코드의 기능을 파악할 수 없었다. 이를 통해 주석, 명확한 변수명 등을 통해 가독성이 좋은 코드의 필요성을 느꼈다.

 

개선 코드

// 품질을 증가시키는 메소드, amount만큼 증가
protected void increaseQuality(int amount) {
    if (gildedRoseItem.quality < MAX_QUALITY) {
        gildedRoseItem.quality = Math.min(gildedRoseItem.quality + amount, MAX_QUALITY);
    }
}

quality를 amount로 변경하여 변수의 의미를 더욱 명확하게 표현했다. 또한, 각 메소드의 기능을 주석으로 명시해 누구나 쉽게 이해할 수 있도록 했다. 정의된 상수를 활용함으로써 코드의 일관성과 유지보수성도 개선되도록 하였다.

 

이러한 개선을 통해 코드의 가독성이 크게 향상된 것을 느낄 수 있었다. 앞으로는 나만의 관점에서 벗어나, 다른 사람이 코드를 보았을 때도 쉽게 이해할 수 있도록 작성하는 연습을 이어가야겠다.

 

객체 생성과 비즈니스 로직 분리

 

기존 코드

class GildedRose {

		... 

    public void updateQuality() {
        for (Item item : items) {
            GildedRoseItem gildedRoseItem = getGildedRoseItem(item);
            gildedRoseItem.update();
        }
    }

		... 
		
    private GildedRoseItem getGildedRoseItem(Item item) {
        switch (item.name) {
            case "Aged Brie":
                return new AgedBrie(item);
            case "Backstage passes to a TAFKAL80ETC concert":
                return new BackstagePasses(item);
            case "Sulfuras, Hand of Ragnaros":
                return new Sulfuras(item);
            case "Conjured Mana Cake":
                return new Conjured(item);
            default:
                return new Normal(item);
        }
    }
}

이 코드를 짜고 얼마 후 해당 리뷰도 보고 코드를 다시 보니 얼마 전 학교 수업에서 배웠던 클래스의 단일 책임 원칙이 떠올랐다. 내 코드는 객체를 생성하는 로직과 Quality를 업데이트하는 로직 등이 여러 개 포함되어 있다.

 

클래스가 여러 역할(로직)을 포함하고 있으므로, 클래스의 단일 책임 원칙을 위배하고 있는 것이다. 클래스의 item이 더 추가될 수록 클래스는 비대해지고 update를 하는 부분까지 영향을 받으므로 유지 보수가 어려울 거 같았다.

 

 

SOLID 원칙, 어렵지 않다!

객체지향 프로그래밍 설계 원칙에 대해 알아보기

velog.io

 

개선 코드

// GildedRoseItem에 대한 비즈니스 로직을 처리하는 역할을 담당
class GildedRose {

	  ...

    public void updateQuality() {
        for (Item item : items) {
            GildedRoseItem gildedRoseItem = GildedRoseItemFactory.create(item);
            gildedRoseItem.update();
        }
    }

		...

}
// GildedRoseItem에 대하여 객체 생성을 담담
public class GildedRoseItemFactory {

    public static GildedRoseItem create(Item item) {
        switch (item.name) {
            case "Aged Brie":
                return new AgedBrie(item);
            case "Backstage passes to a TAFKAL80ETC concert":
                return new BackstagePasses(item);
            case "Sulfuras, Hand of Ragnaros":
                return new Sulfuras(item);
            case "Conjured":
                return new Conjured(item);
            default:
                return new Normal(item);
        }
    }
}

 

객체 생성만을 담당하는 클래스와 객체에 대하여 값을 초기화하고, 업데이트 하는 클래스를 분리하여 각 클래스 별로 하나의 책임만을 가지도록 분리하였다. 이렇게 각 클래스를 역할 별로 분리해보니 각 클래스가 독립적으로 확장 가능하고 유지보수성이 높아졌음을 확인할 수 있었다.

 

객체 생성을 처리하는 메소드는 static 메소드로 설정하였다. 객체 생성만을 담당하는 클래스이므로 불필요하게 인스턴스를 생성하여 메모리를 차지하는 것은 좋지 않다고 생각하여 static으로 설정하여 객체 생성 전용 클래스로 설정하였다.

 

회고

 

이번 과제를 통해 코드의 유지보수성, 가독성, 재사용성에 대해 깊이 고민해볼 수 있었다. 단순히 기능 구현에만 집중하기보다는, 협업하는 사람들도 쉽게 이해하고 수정할 수 있는 코드, 확장, 수정하기 용이한 코드를 작성하는 것이 중요함을 느꼈다. 앞으로는 다양한 관점에서의 코드 작성 연습을 통해, 더욱 명확하고 이해하기 쉬운 코드를 구현하는 데 중점을 두어야겠다.

 

PR 링크

https://github.com/IT-Cotato/10th-BE-Networking-1/pull/13

'Dev > SpringBoot' 카테고리의 다른 글

동시성 제어  (0) 2025.02.11
대량의 데이터 저장하기 - Bulk Insert  (0) 2025.02.11
default_batch_fetch_size  (0) 2025.02.10
@Transient  (0) 2025.02.10
인증된 사용자 정보 추출 커스텀 어노테이션 생성  (1) 2025.02.10