티스토리 뷰
스프링 프레임워크(이하 줄여서 스프링)의 핵심 목표는 '자바 개발 간소화' 이다.
객체지향 프로그래밍을 공부한 사람들이라면 느슨한 결합이나 재사용에 대해 들어봤을 것이다.
스프링은 그런 객체지향 프로그래밍을 지원하기 위해 나온 프레임워크들 중 하나다.
앞서 말했듯이 자바 개발 간소화를 하기 위해 스프링의 전략은 다음과 같다.
- POJO를 이용한 가볍고 non-invasive 개발
- DI와 인터페이스 지향을 통한 느슨한 결합도
- AOP와 공통 규약을 통한 선언적 프로그래밍
- 템플릿을 통한 상투적인 코드 축소
1-1. POJO란 무엇인가?
POJO(Plain Old Java Object)란 평범한 자바 객체이다. 마틴 파울러가 2000년 가을 컨퍼런스에서 처음 만들어낸 말이다. 마틴 파울러는 EJB(Enterprise JavaBean) 보다 단순한 자바 객체에 도메인 로직을 넣어 사용하는 것이 여러 장점이 있는데도 왜 사람들이 사용하기를 꺼려하는지에 대해 의문을 가졌다. 그리고 그는 단순한 객체에는 EJB와 같은 그럴듯한 이름이 없어서 그 사용을 주저하는 것이라고 결론을 내렸다. 그래서 만든 것이 POJO란 용어다. POJO기반의 기술을 사용한다고 말하면 왠지 첨단 기술을 사용하는 개발자인 듯한 인상을 주기 때문이다.
특히 스프링은 EJB의 복잡한 개발을 겨냥해 만들어졌다. POJO와 EJB를 비교해 보자. [source 1.1]을 보면 알겠지만 EJB 2.1은 침략적인 API로, 일반적으로 필요하지도 않은 메소드를 강제로 구현하게 한다.
[source 1.1]
package com.imissyoubrad.ejb.session; import javax.ejb.SessionBean; import javax.ejb.SessionContext; public class HelloWorldBean implements SessionBean { public void ejbActivate() { //왜 이런 메소드가 필요할까? } public void ejbPassivate() {} public void ejbRemove() {} public void setSessionContext(SessionContext ctx) {} public String sayHello() { //EJB의 핵심 비지니스 로직 return "Hello World"; } public void ejbCreate() {} }
EJB 2 시대의 Stateless session bean(무상태 세션 빈)이다. 하지만 필요하지 않음에도 불구하고 SessionBean 인터페이스가 EJB의 생명주기에 많은 영향을 끼친다.(이러한 메소드는 'ejb'로 시작한다) 실제로 HelloWorldBean의 대부분의 코드는 전적으로 프레임워크를 위한 용도다. 과연 구가 누구를 위해 일하는 것으로 보이는가?
그렇다면 스트럿츠(Struts)나 웹워크(WebWork), 그리고 태피스트리(Tapestry)의 초기 버전 등도 간단한 자바 클래스가 아니라 자신만의 프레임워크를 강요했다. 이러한 프레임워크는 개발자에게 코드가 흩어져 있는 클래스 작성과 프레임워크에 묶어두고, 테스트 수행도 어려운 것을 강요한다.
반면 스프링은 API를 이용하여 어플리케이션 코드의 분산을 가능한 막는다. 스프링에 특화된 인터페이스 구현이나 스프링에 특화된 클래스 확장을 거의 요구하지 않는다. 또한 스프링이 사용한다는 표식도 거의 없다. 최악의 경우, 클래스에 스프링의 어노테이션(anootation)이 붙지만 그렇지 않은 경우엔 POJO다. 스프링은 [source 1.2]와 같이 불합리한 요구를 하지 않는다.
[source 1.2]
package com.imissyoubrad.spring; public class HelloWorldBean { public String sayHello() { //이것이 실제로 필요한 전부다. return "Hello World"; } }
더 나아지지 않았는가? 정신없던 생명주기 메소드는 모두 사라졌다. 실제로 HelloWorldBean은 구현하지도, 확장하지도 않았고, 심지어 스프링 API에서 어떤 것도 임포트하지 않았다. 스프링의 HelloWorldBean은 군살이 없고, 평범하며, 모든 구문은 POJO다.
1-2. DI 소개 DI를 적용하면 코드가 훨씬 간단해지고, 이해하기 쉬우며, 테스트하기도 쉬워진다는 장점이 있다. 실제 어플리케이션에서는 두 개 이상의 클래스를 사용한다. 이때 각 객체는 협력하는 객체에 대한 레퍼런스를 얻을 책임을 진다. 그 결과, 결합도가 높아지고 테스트하기 힘든 코드가 만들어지기 쉽다. 의존하는 객체를 코드에 직접 명시하는 경우 [source 1.3]과 같아질 것이다.
[source 1.3]
package com.imissyoubrad.poker public class Poker implements Cards { private FullHouseScore card; public Poker() { //FullHouseScore와 강하게 결합된다. card = new FullHouseScore(); } public void PlayCard() throws Exception { card.play(); } }
보다시피 생성자 안에 FullHouseScore를 생성한다. 이것은 TexasPoker 클래스가 FullHouseScore 클래스에 강하게 결합되도록 하며, 기능을 심각하게 제한한다. 게다가 단위 테스트를 작성하기도 몹시 어렵다. 이러한 문제점을 DI를 통해 해결할 수 있다. DI를 이용하면 객체는 시스템에서 각 객체를 조율하는 제3자에 의해 생성 시점에 종속 객체가 부여된다. 즉 개발자는 TexasPoker 클래스에서 FullHouseScore card = new FullHouseScore(); 와 같은 코드를 작성할 필요가 없다. [source 1.4]를 살펴보자. 이 클래스는 어떠한 종류의 이벤트가 발생하더라도 유연하게 대처할 수 있을 것이다.
[source 1.4]
package com.imissyoubrad.poker; public class TexasPoker implements Cards { private Score score; public TexasPoker(Score score) { //Score가 주입된다. this.score = score; } public void PlayCard() throws Exception { score.play(); } }
보다시피 Poker 클래스와 달리 TexasPoker 클래스는 생성 시점에 인자로 점수가 부여된다. 이와 같은 종류의 종속객체 주입을 생성자 주입(constructor injection)이라고 한다. 여기서 요점은 TexasPoker 객체가 Score의 특정 구현체에 결합되지 않는다는 사실이다. Score 인터페이스만 구현하기만 하면 어떠한 이벤트가 발생되어도 문제가 되지 않는다. 이것이 DI의 주요 이점인 '느슨한 결합도(loose coupling)'이다.
1-3. AOP 소개
DI가 소프트웨어 컴포넌트의 결합도를 낮춰 준다면, 애스펙ㅌ 지향 프로그래밍은 어플리케이션 전체에 걸쳐 사용되는 기능을 재사용할 수 있는 컴포넌트에 담을 수 있게 해 준다. 예를들어 각 컴포넌트는 대체로 본연의 특정한 기능 외에 로깅이나 트랜잭션 관리, 보안 등의 시스템 서비스도 수행해야 하는 경우가 많다. 주소록에 주소를 등록하는 메소드는 보안 상태가 유지됐는지 아닌지에는 신경 쓸 필요 없이 주소를 등록하는 방법에만 관여하는 것이 좋은 것이다.
AOP는 시스템 서비스를 모듈화해서 컴포넌트에 선언적으로 적용할 수 있게 해 준다. AOP를 이용하면 시스템 서비스에 대해서는 전혀 알지 못하면서 응집도가 높고 본연의 관심사에 집중하는 컴포넌트를 만들 수 있다. 다시 말해 AOP는 POJO를 말 그대로 평범하게 해 준다.
어떤 사용자가 포커게임을 하기 전에 배팅 금액과 포커게임 후 이겼을 때 그 업적을 남기고 싶다고 가정해 보자.
[source 1.5]
package com.imissyoubrad.poker; public class Logging { public void bet(int money) { System.out.println("You bet: " + money); } public void win() { System.out.println("Yeah! I win!!"); } }
[source 1.5]는 두 개의 메소드가 있는 간단한 클래스이다. bet() 메소드는 게임 시작하기 전에 호출되며, win() 메소드는 게임이 끝난 후 호출된다. 코드에서 이 작업하기 위해서는 간단해야 한다. 따라서 Logging 클래스를 사용하기 위해 TexasPoker 클래스를 적절히 수정하자. 첫 번째 시도는 다음과 같다. 여기서 Logging의 메소드를 호출해야 하는 TexasPoker를 볼 수 있다.
[source 1.6]
package com.imissyoubrad.poker; public class TexasPoker implements Cards { private Score score; private Logging log; public TexasPoker(Score score, Logging log) { this.score = score; this.log = log; } public void PlayCard() throws Exception { log.bet(500); score.play(); log.win(); } }
[source 1.6]의 일부는 적절해 보이지 않는다. 게다가 TexasPoker는 Logging 클래스를 알아야 하므로, 강제로 주입해야한다. 이는 TexasPoker를 복잡하게 만들 뿐만 아니라 Logging을 필요로하지 않는 사용자라면 어떻게 해야 할까? 이런 상황을 위해 null 체크 로직을 도입해야 할까?
이러한 문제점을 처리할 수 있는 것이 AOP이다. 따라서 TexasPoker는 Logging의 메소드를 직접 처리하는 일에서 해방될 수 있는 것이다. Logging을 AOP로 바꾸려면 스프링 설정 파일에 선언하기만 하면 된다. 여기서는 Poker.xml 파일을 수정하겠다.
[source 1.7]
<bean id="texasPoker" class="com.imissyoubrad.poker.TexasPoker">
<constructor-arg ref="score" />
</bean>
<bean id="playCard" class="com.imissyoubrad.poker.PlayCard" />
<!-- Logging bean 선언 -->
<bean id="logging" class="com.imissyoubrad.poker.Logging" />
<aop:config>
<aop:aspect ref="logging">
<!-- 포인트컷 정의 -->
<aop:pointcut id="play" expression="execution(* *.PlayCard(..))" />
<!-- 비포 어드바이스 선언 -->
<aop:before pointcut-ref="play" method="bet" />
<!-- 애프터 어드바이스 선언 -->
<aop:after pointcut-ref="play" method="win" />
</aop:aspect>
</aop:config>
여기서 스프링의 aop 설정 네임스페이스를 사용하여 Logging 빈이 애스펙트라고 선언한다. 먼저 Logging을 빈으로 선언해야 한다. 다음 aop:aspect 엘리먼트에서 빈을 참조한다. 애스펙트를 더 정의해 보자면, PlayCard()의 메소드가 실행되기 전에 Logging의 bet() 메소드가 호출되어져야 한다고 선언한다. 이것을 before advice라 부른다. 그리고 PlayCard()의 메소드가 실행된 후에 win() 메소드가 호출되어야 한다고 선언한다. 이것을 after advice라 부른다.
양쪽 경우 모두 pointcut-ref 어트리뷰트는 play라는 이름의 포인트컷을 참조한다. 이 포인트컷은 앞에 엘리먼트에 어드바이스가 적용될 위치를 선택하는 expression 어트리뷰트와 함께 정의돼 있다. expression 구문은 AspectJ의 포인트컷 표현식 언어다.
여기서 중요한 점을 짚고 넘어간다면 다음 3가지가 있을 것이다.
Logging이 여전히 POJO라는 점이다. Logging이 애스펙트로 사용될 것임을 나타내는 내용은 Logging에 전혀 포함되어 있지 않다. 스프링 컨텍스트에서 선언적으로 애스펙트가 된다.
TexasPoker가 Logging을 명시적으로 호출하지 않았다는 점이다. 실제로 TexasPoker는 Logging의 존재를 전혀 인식하지 못한다.
Logging을 애스펙트로 바꾸기 위해 몇 가지 스프링 마법을 사용했지만, 먼저 으로 선언돼야 한다는 사실이다.
1-4. 템플릿을 이용한 상투적인 코드 제거
안타깝게도 자바 API에는 상투적인 코드가 많이 포함돼 있다. 상투적 코드의 대표적 예는 데이터베이스를 조회하는 JDBC 작업이다. 예를들어 다음과 유사한 코드를 계속 작성해야 하는 경우가 있을 것이다.
[source 1.8]
public Employee getEmployeeById(long id) { Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = dataSource.getConnection(); stmt = conn.prepareStatement( "SELECT id, firstname, lastname, salay FROM employee WHERE id = ?"); stmt.setLong(1, id); rs = stmt.executeQuery(); Employee employee = null; if (rs.next()) { employee = new Employee(); //데이터로부터 객체 생성 employee.setId(rs.getLong("id")); employee.setFirstName(rs.getString("firstname")); employee.setLastName(rs.getString("lastname")); employee.setSalary(rs.getBigDecimal("salay")); } return employee; } catch (Exception ex) { //여기서 무엇을 해야 할까? } finally { if (rs != null) { //정리 작업 try { rs.close(); } catch (Exception e) {} } if (stmt != null) { try { conn.close(); } catch (SQLException e) {} } } return null; }
보다시피 JDBC 코드는 직원의 이름과 급여를 데이터베이스에서 조회한다. 하지만 조회하는 부분을 찾으려면 열심히 찾아야한다. 이유는 직원 조회 코드가 JDBC 형식에 묻혀버렸기 때문이다. [source 1.8]에서 주목할 부분은 거의 모든 JDBC 작업을 위해 작성했던 코드와 완벽히 동일하다는 사실이다. 직원 조회를 위해 수행하는 작업은 극히 일부분이다.
이와 같은 코드를 스프링은 템플릿에 캡슐화하여 반복적인 코드를 제거하는 방법을 통해 해결한다. 스프링의 JdbcTemplate은 전통적인 JDBC에서 필요한 모든 형식 없이도 데이터베이스 작업을 수행할 수 있게 한다. 예를 들어, [source 1.9]와 같이 직원 데이터를 조회하는 작업에 초점을 맞추도록 getEmployeeById() 메소드를 업데이트할 수 있다.
[source 1.9]
public Employee getEmployeeById(long id) { //쿼리 파라미터 지정 return jdbcTemplate.queryForObject( "SELECT id, firstname, lastname, salay " + "FROM employee WHERE id=?", new RowMapper() { public Employee mapRow(ResultSet rs, int rowNum) throws SQLException { Employee employee = new Employee(); employee.setId(rs.getLong("id")); employee.setFirstName(rs.getString("firstname")); employee.setLastName(rs.getString("lastname")); employee.setSalay(rs.getBigDecimal("salay")); return employee; } }, id); }
보다시피 새롭게 수정된 getEmployeeById()는 매우 간단하면서도 실제로 데이터베이스에서 직원을 조회하는 작업에 추점을 맞춘다. 템플릿의 queryForObject() 메소드에는 SQL 쿼리, RowMapper(결과 집합 데이터를 도메인 객체에 매핑), 그리고 쿼리 파라미터가 부여될 수 있다. getEmployeeById()에서는 이전에 보였던 JDBC의 상투적인 코드가 보이지 않는다. 이것은 모두 템플릿 내부에서 처리된다.
지금까지 POJO 지향 개발, DI, AOP, 그리고 템플릿을 이용하여 자바 개발의 복잡성을 공략하는 방법을 살펴봤다. 또한 XML 기반 설정 파일에서 빈과 에스펙트를 설정하는 방법도 살펴봤다. 그런데 이러한 파일을 어떻게 로드할 수 있을까? 그리고 어디에 로드될까? 다음 장은 어플리케이션의 빈이 위치하는 스프링 컨테이너를 살펴보기로 한다.
'프로그래밍 > Spring' 카테고리의 다른 글
[Spring]스프링 속으로 (0) | 2017.11.22 |
---|