모르지 않다는 것은 아는것과 다르다.

객체지향

상속 보다는 합성을 사용하라!

채마스 2022. 3. 10. 23:19

상속

 

  • 위의 사진처럼 MovieFinder 라는 부모 클래스가 몇몇 기능의 구현을 자식클래스에게 위임한다.
  • 상속을 사용하면 부모 클래스의 멤버를 재사용할 수 있는 장점이 있다.
  • 또한 확장에 용이하다. 만약 요구사항으로 인해 구현체가 늘어났다고 해서 MovieFinder 가 변하지 않기 때문이다.
  • 하지만, 상속은 캡슐화를 위배하고, 설계를 유연하지 못하게 만든다는 단점이 있다.

 

합성

 

  • 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 의미한다.
  • SOLID 원칙중 계방 폐쇄의 원칙(OCP) 을 만족할 수 있다. -> 소프트웨어는 객체는 확장에는 열려있고, 변경에는 닫혀 있어야 한다.
    • MovieReader 를 통해서 자유롭게 확장할 수 있다. 또한 변경시 MovieFinder 는 변경할 필요가 없기 때문이다.
  • Effective Java 에서도 상속보다는 합성을 사용하고, 추상 클래스보다는 인터페이스를 사용하라고 나와있다.
  • 또한 객체는 인터페이스를 통해서 참조하라고 언급 되어있다.

 

코드예시

상속 코드

public abstract class MovieFinder{
    private final MovieReader movieReader;

    public MovieFinder(MovieReader movieReader) {
        this.movieReader = Objects.requireNonNull(movieReader);
    }

    /**
     * 저장된 영화 목록에서 감독으로 영화를 검색한다.   
     * 
     * @param directedBy 감독
     * @return 검색된 영화 목록
     */
    public List<Movie> directedBy(String directedBy) {
        return movieReader.loadMovies()
                          .stream()
                          .filter(it -> it.getDirector().toLowerCase().contains(directedBy.toLowerCase()))
                          .collect(Collectors.toList());        
    }

    /**
     * 저장된 영화 목록에서 개봉년도로 영화를 검색한다.
     * 
     * @param releasedYearBy
     * @return 검색된 영화 목록
     */
    public List<Movie> releasedYearBy(int releasedYearBy) {
        return movieReader.loadMovies()
                          .stream()
                          .filter(it -> Objects.equals(it.getReleaseYear(), releasedYearBy))
                          .collect(Collectors.toList());        
    }

    public abstract List<Movie> loadMovies();    
}
  • MovieFinder 를 추상 클래스로 설정하고, loadMovies 메소드를 추상메소드로 등록한다.

 

public class CsvMoviesFinder extends MovieFinder {

    /**
     * 영화 메타데이터를 읽어 저장된 영화 목록을 불러온다.
     * 
     * @return 불러온 영화 목록
     */
    @Override
    public List<Movie> loadMovies() {
        try {
            final InputStream content = getMetadataResource().getInputStream();
            final Function<String, Movie> mapCsv = csv -> {
                try {
                    // split with comma
                    String[] values = csv.split(",");

                    String title = values[0];
                    List<String> genres = Arrays.asList(values[1].split("\\|"));
                    String language = values[2].trim();
                    String country = values[3].trim();
                    int releaseYear = Integer.valueOf(values[4].trim());
                    String director = values[5].trim();
                    List<String> actors = Arrays.asList(values[6].split("\\|"));
                    URL imdbLink = new URL(values[7].trim());
                    String watchedDate = values[8]; 

                    return Movie.of(title, genres, language, country, releaseYear, director, actors, imdbLink, watchedDate);
                } catch(IOException error) {
                    throw new ApplicationException("mapping csv to object failed.", error);
                }
            };

            return new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))
                    .lines()
                    .skip(1)
                    .map(mapCsv)
                    .collect(Collectors.toList());
        } catch(IOException error) {
            throw new ApplicationException("failed to load movies data.", error);
        }
    }
}
  • CsvMoviesFinder 는 MovieFinder 를 상속받고 loadMovies 를 구현한다.

 

public class XmlMoviesReader extends MovieFinder {

    @Override
    public List<Movie> loadMovies() {
        try {
            final InputStream content = getMetadataResource().getInputStream();
            final Source source = new StreamSource(content);
            final MovieMetadata metadata = (MovieMetadata) unmarshaller.unmarshal(source);

            return metadata.toMovies();
        } catch (IOException error) {
            throw new ApplicationException("failed to load movies data.", error);
        }
    }
}
  • XmlMoviesFinder 또한 MovieFinder 를 상속받고 loadMovies 를 구현한다.
MovieFinder movieFinder = new CsvMovieFinder();
List<Movie> result = movieFinder.directedBy("Michael");
assertEquals(3, result.size());

result = movieFinder.releasedYearBy(2015);
assertEquals(225, result.size());
  • 위와 같이 상속을 적용할 수 있다.
  • 이렇게 되면 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에서 위임할 수 있다.
  • 이러한 구조를 디자인 패턴에서는 템플릿 메소드 패턴이라고 한다.
  • 하지만 상속보단 합성을 적용하는 것이 바람직하다. 위의 코드를 합성으로 바꿔보자.

 

합성 예시

public interface MovieReader {

    List<Movie> loadMovies();
}
public class CsvMovieReader implements MovieReader {

    /**
     * 영화 메타데이터를 읽어 저장된 영화 목록을 불러온다.
     * 
     * @return 불러온 영화 목록
     */
    @Override
    public List<Movie> loadMovies() {
        try {
            final InputStream content = getMetadataResource().getInputStream();
            final Function<String, Movie> mapCsv = csv -> {
                try {
                    // split with comma
                    String[] values = csv.split(",");

                    String title = values[0];
                    List<String> genres = Arrays.asList(values[1].split("\\|"));
                    String language = values[2].trim();
                    String country = values[3].trim();
                    int releaseYear = Integer.valueOf(values[4].trim());
                    String director = values[5].trim();
                    List<String> actors = Arrays.asList(values[6].split("\\|"));
                    URL imdbLink = new URL(values[7].trim());
                    String watchedDate = values[8]; 

                    return Movie.of(title, genres, language, country, releaseYear, director, actors, imdbLink, watchedDate);
                } catch(IOException error) {
                    throw new ApplicationException("mapping csv to object failed.", error);
                }
            };

            return new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))
                    .lines()
                    .skip(1)
                    .map(mapCsv)
                    .collect(Collectors.toList());
        } catch(IOException error) {
            throw new ApplicationException("failed to load movies data.", error);
        }
    }

}
public class XmlMovieReader implements MovieReader {

    @Override
    public List<Movie> loadMovies() {
        try {
            final InputStream content = getMetadataResource().getInputStream();
            final Source source = new StreamSource(content);
            final MovieMetadata metadata = (MovieMetadata) unmarshaller.unmarshal(source);

            return metadata.toMovies();
        } catch (IOException error) {
            throw new ApplicationException("failed to load movies data.", error);
        }
    }

}
public class MovieFinder {

    private MovieReader movieReader = new CsvMovieReader();

    /**
     * 저장된 영화 목록에서 감독으로 영화를 검색한다.   
     * 
     * @param directedBy 감독
     * @return 검색된 영화 목록
     */
    public List<Movie> directedBy(String directedBy) {
        return movieReader.loadMovies()
                          .stream()
                          .filter(it -> it.getDirector().toLowerCase().contains(directedBy.toLowerCase()))
                          .collect(Collectors.toList());        
    }

    /**
     * 저장된 영화 목록에서 개봉년도로 영화를 검색한다.
     * 
     * @param releasedYearBy
     * @return 검색된 영화 목록
     */
    public List<Movie> releasedYearBy(int releasedYearBy) {
        return movieReader.loadMovies()
                          .stream()
                          .filter(it -> Objects.equals(it.getReleaseYear(), releasedYearBy))
                          .collect(Collectors.toList());        
    }    

}
  • 앞서 언급했듯이, 객체를 MovieReader 라는 인터페이스를 통해서 참조 하고있다.
  • 이렇게 인터페이스를 통해서 합성하면 구현을 효과적으로 캡슐화 할 수 있다.
  • 또한 참조되는 인스턴스를 교체하기 쉽기 때문에 설계를 유연하게 만들어 준다.
  • 이러한 구조를 디자인 패턴에서는 전략 패턴이라고 한다.
  • 따라서 상속을 합성으로 대체할 수 있다면 대체하는 것이 바람직하다.




REFERENCES

  • 박용권님의 스프링러너 스프링 아카데미