카테고리 없음

[디자인패턴] 객체지향 설계 원칙 SOLID

재인꾸 2021. 8. 21. 01:03
반응형

회사에 입사한지 1년이 넘어가는 JAVA 개발자지만

기존의 시스템을 운영/개발하면서 이전 개발자 분이 어떤 구조로 설계하셨는지, 내 코드가 그 설계를 해치지는 않는지 의구심이 들 때가 있다.

객체지향 설계 원칙을 공부하면서 생각에 확신을 더 갖고자 한다.

 

원칙을 지키는 사람이 되자 . . 화이팅 . .

 


객체지향 설계 5가지 원칙 SOLID

객체지향의 관점에서 자기 자신 클래스 안의 응집도는 높이고, 타 클래스 간 결합도는 낮추는 원칙

 

- SRP (Single Responsibility Principle) : 단일 책임 원칙

- OCP (Open-Closed Principle) : 개발 폐쇄 원칙

- LSP (Liskov Substitution Principle) : 리스코프 치환 원칙

- ISP (Interface Segragation Principle) : 인터페이스 분리 원칙

- DIP (Dependency Inversion Principle) : 의존관계 역전 원칙

 

읽자마자 와닿기 어려운 단어들이다.

아래에서 자세히 알아보자 !

 

SRP(Single Responsibility Principle), 단일 책임 원칙

하나의 객체는 하나의 책임만 가져야한다

 

책임이라는 기준이 모호하기 때문에 변경을 책임의 기준으로 생각하자

 

어떠한 역할에 대해 변경사항이 발생했을 때,

SRP 원칙이 잘 적용된 클래스의 경우 간단하게 수정이 가능할 것이다.

하지만 여러 책임을 가진 클래스의 경우 변경 포인트가 많을 것이다.

 

이처럼 변경사항이 있을 때 어플리케이션의 파급효과가 적으면 SRP 원칙을 작 따른것으로 볼수있다.

 

OCP(Open-Closed Principle), 개발 폐쇄 원칙

소프트웨어 엔티티(클래스, 모듈, 함수 등)는 자신의 확장에 대해서 열려 있고, 주변의 변화에 대해서 닫혀있어야 한다.

 

다형성을 떠올려보자 !

이를 적용하기 위해서는 각 모듈 간 호출, 의존에 대해서 Concrete Class 가 아닌 Interface 또는 추상화에 의존하도록 설계되어야 하며 코드를 매번 수정하지 않고도 새로운 상황에 적응할 수 있도록 개발가능하다.

 

LSP(Liskov Substitution Principle), 리스코프 치환 원칙

서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.

 

LSP는 상속의 기본적인 메커니즘을 표현하며, 상속은 다음의 조건을 만족해야한다.

  • 하위 클래스 is a kind of 상위 클래스 : 하위 분류는 상위 분류의 한 종류다.
  • 구현 클래스 is able to 인터페이스 : 구현 분류는 인터페이스 할 수 있어야 한다.

 

다형성에서 하위클래스가 인터페이스 규약을 다지킨다면,

사용자의 관점에서 기능에 영향을 미치지 않고 언제든지 서브 클래스를 부모 클래스로 대체 가능하다.

 

ISP(Interface Segragation Principle), 인터페이스 분리 원칙

한 클래스는 자신이 사용하지 않는 인터페이스를 구현하지 말아야 한다.

 

B 클래스가 A 클래스를 상속받았을 때 최소한의 인터페이스만을 사용해야 하므로

한개의 인터페이스보다 구체적인 여러 인터페이스를 구현하자는 원칙이다.

 

SRP가 클래스의 단일책임을 강조한다면, ISP는 인터페이스의 단일책임을 강조한다.

 

DIP(Dependency Inversion Principle), 의존관계 역전 원칙

고차원 모듈은 저차원 모듈에 의존하면 안된다.

 

추상화된 것은 구체적인 것에 의존하면 안된다는 원칙으로,

자주 변경되는 구체(Concrete) 클래스에 의존하지 말아야 한다.

 

예시로 스프링 JDBC 사용하는 경우를 살펴보자.

회원관리 시스템의 id를 통한 회원정보 조회 부분을 가져왔다.

 

아직 데이터베이스가 정해지지 않은 상황에서 MemberRepository 라는 인터페이스를 만든다.

public interface MemberRepository {
	Member save(Member member);
	Optional<Member> findById(Long id);
	Optional<Member> findByName(String name);
	List<Member> findAll();
}

 

위의 MemberRepository 를 구현한 세가지 클래스가 있다.

 

MemoryMemberRepository.class

코드 실행 시에만 HashMap 형태로 저장

 

@Repository
public class MemoryMemberRepository implements MemberRepository {
	
	private static Map<Long, Member> store = new HashMap<>();
	private Long sequence = 0L;

	@Override
	public Optional<Member> findById(Long id) {
		// Optional 은 Null인 객체를 감싸서 리턴 가능함 (JAVA8 부터 추가된 기능)
		return Optional.ofNullable(store.get(id));
	}

	public void clearStore() {
		store.clear();
	}
}

 

JdbcMemberRepository.class

 

Spring-Jdbc library를 사용해 데이터 베이스에 저장

순수 Jdbc 사용 

public class JdbcMemberRepository implements MemberRepository {
	private final DataSource dataSource;

	public JdbcMemberRepository(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	@Override
	public Optional<Member> findById(Long id) {
		String sql = "select * from member where id = ?";
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		
		try {
			conn = getConnection();
			
			pstmt = conn.prepareStatement(sql);
			pstmt.setLong(1, id);
			
			rs = pstmt.executeQuery();
			if (rs.next()) {
				Member member = new Member();
				member.setId(rs.getLong("id"));
				member.setName(rs.getString("name"));
				return Optional.of(member);
			} else {
				return Optional.empty();
			}
		} catch (Exception e) {
			throw new IllegalStateException(e);
		} finally {
			close(conn, pstmt, rs);
		}
	}
	
	// DataSourceUtils를 통해서 connection을 받아야 database connection을 똑같이 해줌 
	private Connection getConnection() {
		return DataSourceUtils.getConnection(dataSource);
	}

	private void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
		try {
			if (rs != null) {
				rs.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		try {
			if (pstmt != null) {
				pstmt.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		try {
			if (conn != null) {
				close(conn);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	private void close(Connection conn) throws SQLException {
		DataSourceUtils.releaseConnection(conn, dataSource);
	}
}

 

JdbcTemplateMemberRepository.class

Spring-JdbcTemplate library를 사용해 데이터 베이스에 저장

순수 JDBC의 반복 코드(pstmt, rs 관련) 중복을 제거

 

public class JdbcTemplateMemberRepository implements MemberRepository {

	private final JdbcTemplate jdbcTemplate;

	// @Autowired
	// 생성자가 하나일때는 Autowired annotation 생략이 가능하다.
	public JdbcTemplateMemberRepository(DataSource dataSource) {
		jdbcTemplate = new JdbcTemplate(dataSource);
	}
    
	@Override
	public Optional<Member> findById(Long id) {
		List<Member> result = jdbcTemplate.query("SELECT * FROM MEMBER WHERE ID = ?", memberRowMapper(), id);
		// Optional 로 변환
		return result.stream().findAny();
	}
}

 

아래는 SpringConfig에서 빈 주입 시 사용되는 코드이다.

 

@Configuration
public class SpringConfig {
	
	private DataSource dataSource;
	
	@Autowired
	public SpringConfig(DataSource dataSource) {
		this.dataSource = dataSource;
	}
	
	@Bean
	public MemberRepository memberRepository() {
		//return new MemoryMemberRepository();
		//return new JdbcMemberRepository(dataSource);
		return new JdbcTemplateMemberRepository(dataSource);
	}
}

 

repository 사용 시 추상클래스인 MemberRepository를 반환함으로써

별다른 변경 없이 구현체만 바꿔서 사용할 수 있게 한다.

 

핵심은 의존 관계를 맺을 때 변화하기 쉬운 것에 의존하기보다 변화하지 않는 것에 의존해야한다는 점이다 !

소스코드를 통해 알 수 있듯이 여러 구현체를 통해 확장성이 용이하며 객체간의 관계를 느슨하게 해주는 장점을 가지기 때문에

다양한 설계 방식, 복잡한 시스템 설계가 가능해진다.

 

하지만
return new JdbcTemplateMemberRepositoy 소스 부분에서
어쩔 수 없이 구현체 변경 시마다 소스 변경이 필요해지고, 이는 DIP를 위반한 것이된다.

다형성만으로 OCP, DIP를 지킬 수 없으며 추가적으로 뭔가 더 필요하다.

reference

https://huisam.tistory.com/entry/DIP

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

https://youtu.be/nrlHZBAXjv8

반응형