강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

hello님의 프로필 이미지
hello

작성한 질문수

오브젝트 - 설계 원칙편

8-5. 리스코프 치환 원칙을 위한 가이드

8-5 책임 분리를 통한 중복 코드 제거 과정에서 특정 조합은 불가능할 때

해결된 질문

작성

·

18

·

수정됨

1

안녕하세요!

AbstractReader로부터 파싱에 대한 책임을 분리하여 Parser라는 클래스를 새로 생성하는 내용 잘 들었습니다!
여기서 궁금한 것이 생겨 질문 남깁니다..!

DatabaseReader, RedisReader, FileReader / CsvParser, JsonParser, XmlParser 구성은 총 9가지의 경우의 수가 나올 수 있을 것 같은데요

만약 '(DatabaseReader, XmlParser)는 불가능하고 (DatabaseReader, CsvParser)와 (DatabaseReader, JsonParser)가 가능하고,
(RedisReader, XmlParser)와 (RedisReader, CsvParser)는 불가능하고 (RedisReader, JsonParser)만 가능하다' 와 같이 특정 조합이 불가능한 경우는 어떻게 컴파일 타임에 가능한 조합으로만 강제할 수 있을지 궁금합니다!

답변 2

0

조영호님의 프로필 이미지
조영호
지식공유자

hello님 안녕하세요.

좋은 질문 남겨 주셔서 감사합니다. 🙂

 

컴파일타임에 의존성 주입 가능한 조합을 체크하는 방법 중에서 가장 간단한 방식은 파라미터의 타입을 명시적으로 드러내서 주입받을 수 있는 타입의 종류를 제한하는 것입니다.

간단하게 말해서 DatabaseReader와 RedisReade의 생성자에서 조합 가능한 Parser의 타입을 전달받는 생성자들을 추가하는 방식이죠.

  • (DatabaseReader, XmlParser)는 불가능하고 (DatabaseReader, CsvParser)와 (DatabaseReader, JsonParser)는 가능한 경우

public class DatabaseReader extends AbstractReader {
    public DatabaseReader(String path, CsvParser parser) {
        super(path, parser);
    }

    public DatabaseReader(String path, JsonParser parser) {
        super(path, parser);
    }
    ...
}
  • (RedisReader, XmlParser)와 (RedisReader, CsvParser)는 불가능하고 (RedisReader, JsonParser) 가능

public class RedisReader extends AbstractReader {
    private JedisPool jedisPool;

    public RedisReader(String path, JsonParser parser) {
        super(path, parser);
    }

    public RedisReader(String path, JsonParser parser, JedisPool jedisPool) {
        super(path, parser);
        this.jedisPool = jedisPool;
    }
}

생성자를 private으로 바꾸고 public static 생성 메서드를 추가해도 동일한 효과를 얻을 수 있습니다.

이 방식의 단점은 새롭게 조합 가능한 Parser를 추가하게 되면 Reader 들의 생성자를 함께 수정해야 한다는 점인데, 전체적인 복잡도 관점에서 보면 수용할만한 정도의 단점이라고 생각합니다.

 

만약 새로운 Parser가 추가됐을 때 Reader를 수정하고 싶지 않다면 마커 인터페이스(marker interface)를 사용하는 방법도 있습니다.

DatabaseReader와 조합 가능한 Parser 인터페이스를 제한하기 위해 Parser를 상속받는 DatabaseParsable 인터페이스를 추가합니다.

public interface DatabaseParsable extends Parser {}

RedisReader와 조합 가능한 Parser 인터페이스를 제한하기 위해 RedisParsable 인터페이스를 추가합니다.

public interface RedisParsable extends Parser {}

DatabaseReader는 DatabaseParsable만 조합할 수 있도록 제한합니다.

public class DatabaseReader<T extends DatabaseParsable> extends AbstractReader {
    public DatabaseReader(String path, T parser) {
        super(path, parser);
    }
    ...
}

RedisReader는 RedisParsable만 조합할 수 있도록 제한합니다.

public class RedisReader<T extends RedisParsable> extends AbstractReader {
    private JedisPool jedisPool;

    public RedisReader(String path, T parser) {
        super(path, parser);
    }

    public RedisReader(String path, T parser, JedisPool jedisPool) {
        super(path, parser);
        this.jedisPool = jedisPool;
    }
    ...
 }

DatabaseReader와 조합 가능하지만 RedisReader와는 조합할 수 없는 CsvParser는 DatabaseParsable 인터페이스만 구현합니다.

public class CsvParser implements DatabaseParsable {
  ...
}

DatabaseReader와 RedisReader 양쪽 모두와 조합할 수 있는 JsonParser는 DatabaseParsable와 RedisParsable 인터페이스 양쪽 모두를 구현합니다.

public class JsonParser implements DatabaseParsable, RedisParsable {
  ...
}

이 방식은 생성자를 이용하는 방식과 반대로 Parser가 추가될 때 Reader 계층 클래스를 수정할 필요가 없지만, Reader가 추가되면 조합 방식에 따라 Parser 계층 클래스를 수정해야 합니다.

 

개방-폐쇄 원칙 관점에서 Reader 계층과 Parser 계층 모두를 다 폐쇄시키는 어렵기 때문에 어떤 쪽의 더 중요한 변경의 축인지에 따라 적합한 방식을 선택하시면 될것 같아요.

클래스 계층이 너무 많아져서 이 제약을 클래스 계층 전반에 걸쳐 명시하는게 점점 복잡해진다면 컴파일타임이 아닌 런타임에 제한하는 쪽으로 구현하시는게 전체적인 복잡성을 낮추는 방법일 수 있습니다.

 

답변이 되었는지 모르겠네요. 😊

감사합니다.

0

안녕하세요, 인프런 AI 인턴입니다. hello님!

기존 설계에서 ‘Reader’와 ‘Parser’의 조합으로 총 9가지 경우의 수가 가능하다고 설명하셨고, 특정 조합이 불가능한 경우를 컴파일 타임에 강제하고 싶다고 하셨는데요, 이를 해결하기 위한 방법으로 몇 가지를 제안드리겠습니다.

1. 구체적인 클래스를 이용한 조합 제한

가장 간단한 방법은 각 가능한 조합에 대해 구체적인 클래스를 생성하는 것입니다. 즉, 가능한 조합에 대해서만 해당하는 클래스를 명시적으로 만들어 주는 방식입니다. 이 방법은 경우의 수가 적을 때는 유효할 수 있지만, 경우의 수가 많아진다면 관리가 어려울 수 있습니다.

2. 제너릭 타입과 제한된 다형성

자바에서는 제너릭 타입과 제한된 다형성을 활용하여 가능 조합을 제한할 수 있습니다. 각 Reader가 특정 Parser를 받을 수 있도록 인터페이스 또는 추상 기능을 정의하는 것입니다.

// 인터페이스 정의
interface Parser {}
class CsvParser implements Parser {}
class JsonParser implements Parser {}
class XmlParser implements Parser {}

// 특정 Reader에만 적용 가능한 파서 조합 정의
interface DatabaseCompatibleParser extends Parser {}
class CsvDatabaseParser extends CsvParser implements DatabaseCompatibleParser {}
class JsonDatabaseParser extends JsonParser implements DatabaseCompatibleParser {}

interface RedisCompatibleParser extends Parser {}
class JsonRedisParser extends JsonParser implements RedisCompatibleParser {}

3. 빌더 패턴 이용

빌더 패턴을 활용하여 불가능한 조합을 방지할 수 있습니다. 빌더 내부에서 가능한 조합을 확인하거나 제한할 수 있게 로직을 추가합니다.

4. 검증 함수 사용

런타임에 검증함수를 사용하여 조합을 확인할 수도 있습니다. 다만, 이 방법은 컴파일 타임이 아닌 런타임에 오류를 확인하게 됩니다.

추가 제언

컴파일 타임 강제는 코드의 복잡도를 높일 수 있으므로, 유지보수성과 개발의 용이성을 위해 복잡도가 높은 경우 런타임 검증도 고려해볼 수 있습니다.

관련한 관점에서 Interface Segregation Principle를 적용하여 인터페이스를 잘게 나누는 것도 방법이 될 수 있습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

hello님의 프로필 이미지
hello

작성한 질문수

질문하기