토비의 스프링 7장

doobyeol·2022년 10월 25일
0

7.1 SQL과 DAO의 분리

지금까지의 내용은 UserDao 반복적인 JDBC 작업 흐름을 템플릿을 이용해 제거했다.
DAO에는 깔끔하게 다듬어진 순수한 데이터 액세스 코드만 남게 했다.
하지만, DB테이블과 필드정보를 고스란히 담고 있는 SQL 문장이 남아있다.

public class UserDao {
  JdbcTemplate jdbcTemplate;
  RowMapper<User> userRowMapper;

  public UserDao() {
      this.userRowMapper = (rs, rowNum) -> {
          User user = new User();
          user.setId(rs.getString("id"));
          user.setName(rs.getString("name"));
          user.setPassword(rs.getString("password"));
          return user;
      };
  }

  public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
      this.jdbcTemplate = jdbcTemplate;
  }

  public void add(User user) {
      this.jdbcTemplate.update("insert into users(id, name, password) values (?, ?, ?)"
              , user.getId()
              , user.getName()
              , user.getPassword()
      );
  }

  public User get(String id) {
      return jdbcTemplate.queryForObject("select * from users where id = ?", userRowMapper, id);
  }

  public void deleteAll() {
      jdbcTemplate.update("delete from users");
  }

  public int getCount() {
      return jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
  }

  public List<User> getAll() {
      return this.jdbcTemplate.query("select * from users", userRowMapper);
  }
}

DAO 메소드에서 사용하는 SQL 문장을 UserDao 코드가 아니라 외부 리소스에 담고 이를 읽어와 사용하게 만들면 어떨까? 

이렇게 해두면 DB 테이블의 이름이나 필드 이름을 변경하거나 SQL 쿼리를 최적화해야 할 때도 UserDao 코드에는 손을 댈 필요가 없다. 

어떤 개발팀은 정책적으로 모든 SQL 쿼리를 DBA들이 만들어서 제공하고 관리하는 경우가 있다. 이럴 때 SQL이 독립된 파일에 담겨있다면 편리할 것이다.

→ 관심사의 분리 개념 측면에서 보면
특정한 관심사에 따라 기능을 나누고, 각 기능을 독립적으로 개발한 뒤 이를 조합하는 방식
즉 관심이 같은 것끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하여
특정 관심사에 변경이 일어나면 해당 관심사에만 집중할 수 있도록 설계하는 것이다.

예를 들어 DB 마이그레이션이 일어난 경우
해당 DAO를 참조하는 서비스들은 직접적인 영향을 받기 때문에
서비스 마다 수정을 해주어야 하지만,
DAO와 SQL을 따로 분리했다면 DAO에서 참조로 하는 SQL만 바꿔주면 되기 때문에
수정이 적게 일어나고 수정이 필요한 곳에 집중할 수 있다.

7.2 인터페이스의 분리와 자기참조 빈

XML파일 매핑

  • sql을 저장해두는 독립적인 파일을 이용하자.
  • JAXB는 xml에 담긴 정보를 파일에서 읽어오는 방법 중 하나. xml 정보를 오브젝트처럼 다룰 수 있어 편리함.
    • 언마샬링(unmarchalling) : XML to 자바 오브젝트
    • 마샬링 : 자바 오브젝트 to XML
    • sql맵 xml과 sql 맵을 위한 스키마를 jaxb 컴파일러로 컴파일하면, 바인딩용 클래스가 생성된다.
  • 언제 JAXB를 사용해 XML문서를 가져올까?
    • DAO가 sql 요청할 때마다 매번 xml파일을 다시 읽는건 비효율적. 한번 읽은건 어딘가에 저장해두고 DAO에서 요청이 올 때 사용해야한다.

    • 처음 SQL을 읽어들이는걸 우선은 생성자에서 하고 동작하게 만들어보자. 변환된 SQL오브젝트는 맵에 저장해놨다가 DAO요청이 오면 전달하는 방식으로.

      public class XmlSqlService implements SqlService {
          private Map<String, String> sqlMap = new HashMap<String, String>(); // 읽어온 SQL을 저장해둘 맵
          
          public XmlSqlService() {    // 생성자에서 xml읽어오기
              String contextPath = Sqlmap.class.getPackage().getName();
              try {
                  JAXBContext context = JAXBContext.newInstance(contextPath);
                  Unmarshaller unmarshaller = context.createUnmarshaller();
                  InputStream is = UserDao.class.getResourceAsStream("sqlmap.xml");
                  Sqlmap sqlmap = (Sqlmap)unmarshaller.unmarshal(is);
                  for(SqlType sql : sqlmap.getSql()) {
                      sqlMap.put(sql.getKey(), sql.getValue()); // 읽어온 SQL을 맵으로 저장해둔다.
                  }
              } catch (JAXBException e) {
                  throw new RuntimeException(e);
              }
          }

빈의 초기화 작업

  • 위 코드에서 개선해야할 점
    • 생성자에서 예외가 발생할 수 있는 초기화 작업은 다루지 않는게 좋다.
        @Component
        public class InvalidInitExampleBean {

            @Autowired
            private Environment env;

            public InvalidInitExampleBean() {
                env.getActiveProfiles();
            }
        }

객체가 생성되는 시점에는 아직 env 가 초기화되지 않은 상태이므로 
NullPointerException이 발생하게 된다.

=> 별도의 초기화 메서드를 사용해라

@PostConstruct 어노테이션
@PostConstruct 어노테이션은 특정 클래스의 메소드에 붙여서
해당 클래스의 객체 내 모든 의존성(Bean) 들이 초기화 된 직후에 딱 한 번만 실행되도록 해준다.

        @Component
        public class PostConstructExampleBean {

            private static final Logger LOG 
              = Logger.getLogger(PostConstructExampleBean.class);

            @Autowired
            private Environment environment;

            @PostConstruct
            public void init() {
                LOG.info(Arrays.asList(environment.getDefaultProfiles()));
            }
        }

그 외에도 다양한 초기 로직 셋팅 방법 : https://sgc109.github.io/2020/07/09/spring-running-startup-logic/

-  어느시점에 초기화를 해야할까?
=> 스프링이 제어권을 가지고 있으므로 스프링에게 맡겨야한다.
-  <context:annotation-config/> 설정을 해주면 빈 후처리기들이 등록된다.**
⇒ ApplicationContext 안에 이미 등록된 bean들의 Annotation을 활성화 하기 위해 사용된다.

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:annotation-config/>
    </beans>

-  여기서 초기화는 @PostConstruct 어노테이션을 통해 초기화 메서드로 지정해줄 수 있다.

    public class XmlSqlService implements SqlService {
    
        @PostConstruct // 빈생성 후 초기화 메서드 등록
        public void loadSql()
    
                //....
        }
    }

context:annotation-config 는 @PostConstruct를 붙인 메소드가 빈이 초기화 된후에 자동으로 실행되도록 사용함
@Configuration 사용하면 알아서 빈 후처리기를 등록해주기 때문에 context:annotation-config 필요 없음.

7.3 서비스 추상화

JAXB가 표준에 포함된 라이브러리지만 그 외에도 다양한 XML - 자바오브젝트 매핑 기술이 있다. 이를 OXM이라 부른다. 모두 같은 역할을 하기 때문에 유사한 API를 제공한다. 그래서 스프링에서는 트랜잭션, 메일 전송과 유사하게 OXM에 대해서도 서비스 추상화를 제공한다.

OXM 서비스 인터페이스

스프링이 제공하는 OXM 추상화 서비스 인터페이스에서는
자바오브젝트→XML (Marshaller)
XML → 자바오브젝트 (Unmarshaller)
가 존재한다.

  • OXM
    • OXM(Object-XML Mapping) : XML과 자바 객체를 매핑해서 상호 변환해주는 기술
    • OXM 기술 : Castor XML, JiBX, XmlBeans, Xstream
    • 기능이 같은 여러가지 기술이 존재한다는 이야기가 나오면 서비스 추상화를 할 수 있다.
    • xml파일을 다양한 소스에서 가져올 수있게 한다.

리소스 추상화

자바에서는 http, ftp, file, 클래스패스 내 리소스 등 다양한 리소스에 일관성 있게 접근할 수 있는 방법이 없다. 그래서 스프링에선 Resource라는 추상화 인터페이스를 정의했다.

public interface Resource extends InputStreamSource {
	  boolean exists();
	  default boolean isReadable();
	  default boolean isOpen();
	
	  URL getURL() throws IOException;
	  URI getURI() throws IOException;
	  File getFile() throws IOException;
	
	  Resource createRelative(String relativePath) throws IOException;    
	
	  long lastModified() throws IOException;    
	  String getFilename();
	  String getDescription();
}

7.4 인터페이스 상속을 통한 안전한 기능확장

권장되진 않지만, 서버 운영중에 SQL을 변경해야할 수도 있다.
애플리케이션을 재시작하지 않고 특정 SQL내용만 변경하고 싶다면 어떻게 해야할까?

DI와 인터페이스 프로그래밍

  • DI의 가치는 DI에 적합한 오브젝트 설계를 통해 얻을 수 있다. DI는 런타임 시에 의존 오브젝트를 다이내믹하게 연결해줘서 유연한 확장을 하는게 목적.

  • DI를 적용할 때는 최대한 두 개의 오브젝트가 인터페이스를 통해 느슨하게 연결되어야 한다.

  • 인터페이스를 사용하는 이유

    • 다형성을 얻기 위해 - 하나의 인터페이스를 통해 여러 개의 구현을 바꿔가며 사용할 수 있게!

    • 인터페이스 분리 원칙을 통해 클라이언트와 의존 오브젝트 사이의 관계를 명확하게 해줄 수 있다.

      • ex) B오브젝트가 B1과 B2라는 인터페이스를 구현하고 있고,
        A 오브젝트가 B1을 통해 B 오브젝트를 사용하고 있고있는 경우
        - A는 B1에만 관심이 있는데 B2 인터페이스의 메소드까지 모두 노출되어 B 클래스에 직접 의존할 이유가 없음
        - 따라서 B1을 통해 B를 의존

      • 이렇게 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 분리하는 원칙을 인터페이스 분리 원칙 이라고 부른다.

인터페이스 상속

때로는 인터페이스를 여러 개 만드는 대신 기존 인터페이스를 상속을 통해 확장하는 방법도 사용된다.

  • 위 그림에서 MySqlRegistry의 기본 기능에서 이미 등록된 SQL을 변경하는 기능을 넣어서 확장하고 싶다고 할 때, 어떻게 해야할까?
    • 이미 SqlRegistry의 클라이언트가 있기 때문에, SqlRegistry를 수정하는건 좋지 X.
    • 새롭게 추가할 기능을 사용하는 클라이언트를 위해 기존 SqlRegistry를 상속한 인터페이스를 정의하자.
    • 새로 UpdatableSqlRegistry를 구현한 클래스까지 만든다. 기존 클라이언트인 BaseSqlService, 새로 만든 업데이트 기능을 사용하는 SqlAdminService 둘다 결과적으로 DI받는 구현체는 똑같지만 각각 의존하는 인터페이스는 관심과 필요에 따라 다르게 된다. 이렇게 유연한 확장이 가능함.
    • 잘 적용된 DI는 객체지향적으로 설계된 오브젝트 의존관계에 달려있다.

public interface SqlRegistry {
    void registry();
}
public interface UpdatableSqlRegistry extends SqlRegistry {
    void update();
}
public class MyRegistry implements UpdatableSqlRegistry {
    @Override
    public void registry() {
        // 등록
    }

    @Override
    public void update() {
        // 수정
    }
}
public class BaseSqlService {

    private SqlRegistry sqlRegistry = new MyRegistry();

    void some() {
        sqlRegistry.registry();
    }
}
public class SqlAdminService {
    private UpdatableSqlRegistry updatableSqlRegistry = new MyRegistry();

    void some() {
        updatableSqlRegistry.update();
    }
}

회고

이번 챕터를 리뷰하면서, 나름 스프링이 어떤 디자인 패턴을 지향하는지가 눈에 명확히 보이기 시작한 것 같다.
사실 토비의 스프링 처음 읽기 시작했을 땐 IoC, DI, AOP 에 대해
막연히 어려운 개념이라고 생각했는데, 책을 읽어가면서
반복적인 코드를 자동화하거나, 인터페이스를 통해 오브젝트간의 연결을 느슨히 해주는 등.
코드가 점점 개선되는 것을 보아가면서
결국은 서비스 확장, 수정이 일어날 때 유연한 대처를 하기 위해
특정 관심사에 따라 기능을 분리하는 설계가 필요한 것이고
스프링은 개발자가 핵심적인 비즈니스 로직에만 집중할 수 있도록
조금 번거롭더라도 이런 설계 방식을 지향하고 있고
이를 자동화 시켜주는 편리한 어노테이션들을 제공 하는 것 같다.
어노테이션은 사용하면서도 왜 이런 기능들이 쓰이게 되었는지, 그 등장 배경들은 알지 못했는데
토비의 스프링을 통해 점점 알아가는 것 같고 몰랐던 어노테이션도 새롭게 알게 되어서 유익했다.

😊

2개의 댓글

comment-user-thumbnail
2022년 10월 26일

멋져요!!

1개의 답글