 |  |
|
난이도 : 중급 Ryan Senior, 컨설턴트, Source Allies, Inc. Travis Klotz, 컨설턴트, Source Allies, Inc. Jim Majure , 컨설턴트, Source Allies, Inc.
옮긴이: 박찬욱 dwkorea@kr.ibm.com
2008 년 8 월 05 일 많은 개발자가 애플리케이션의 영속성 티어에서 객체 관계형 매핑(ORM) 도구를 사용하지만, 일부 개발자는 ORM을 어떻게 사용해야 하는지 혼란스러워 해서 불필요한 코드 중복을 만들기도 합니다. 필자는 영속성 티어를 많이 구축해본 경험을 바탕으로 영속성 패턴에 대한 이해와 모범 사례를 확실하게 제시합니다. 2부로 구성된 연재 중 1부인 이번 글에서는 일관성 있고, 간결한 도메인 모델과 영속성 티어를 구축하는 기본을 다룹니다. 2부에서는 이번 글에서 다룬 개념을 구축하고, 확장해 봅니다.
소개
지난 5년에서 10년 동안 개발자들이 엔터프라이즈 애플리케이션에서 영속성 엔티티를 표현하는 방법이 기본적으로 바뀌어 왔다. 일찍이 엔터프라이즈 애플리케이션에서는 모델 엔티티 사이의 관계에 데이터베이스 테이블과 외래 키를 사용했다. 애플리케이션은 데이터베이스를 기반으로 하는 모델에 대한 뷰와 쿼리 실행을 위한 방법으로만 봤다. 최근 몇 년간 경향은 데이터베이스를 기반으로 하는 모델링에서 애플리케이션의 객체 모델을 기반으로 하는 모델링으로 옮겨가고 있다. 이제 데이터베이스는 단순하게 객체 구조에 정의되어 있는 영속성 정보를 저장하는 메커니즘으로 인식되고 있다. 종합적으로 데이터베이스를 기반으로 하는 모델링에서 객체 모델를 기반으로 하는 모델링으로 변경은 아래 내용을 포함하는 많은 장점을 가지고 있다.
- 영속성 엔티티와 엔티티가 수행하는 오퍼레이션의 강한 통합
- 느슨한 결합을 갖는 애플리케이션 컴포넌트를 생성할 수 있는 향상된 능력
- 관계형 데이터베이스에서는 제공하지 않지만 객체 지향에서 허용하는 풍부한 연관
- 특정 데이터베이스 플랫폼에 대한 고도의 독립성
이런 변화가 일어난 배경의 가장 큰 요인은 객체 관계형 매핑(ORM: object-relational mapping) 시스템의 등장이다. ORM이 대상 언어의 관용적인 사용(idiomatic use)을 통해 일관된 방법으로 영속성 객체에 접근할 수 있는 높은 능력을 보여주기 때문이다. 하이버네이트나 TopLink 같은 도구는 객체 모델을 관계형 데이터베이스 스키마로 쉽게 만들어 준다.
이러한 도구가 등장해 ORM 사용이 점점 발전해가고 있다. 처음에는 단지 데이터베이스 테이블에 접근하는 방법으로 ORM 도구에 접근하는 개발자가 많았다. 엔티티를 데이터베이스 테이블과 일 대 일로 매핑했고, 주(primary) 키와 같은 필드 변수는 각 엔티티에 중복해 두었다. 데이터베이스와 엔티티 간의 연관에 대한 행동을 제공하지 못했기 때문에 결국 도메인 모델은 getter와 setter 메서드를 갖는 단순한 변수만 가지고 있었다. 엔티티의 연관을 처리하는 행동은 결국 서비스나 뷰 계층에서 처리하도록 넘겨졌다.
많은 프로젝트에서 ORM 도구를 사용한 경험 덕에 이런 문제에 대해 더 좋은 접근법을 발견하게 되었다. 영속화에 관계 없이, 도메인 모델로 행동하도록, 도메인을 완전하게 따로 두는 것이 더 좋다는 생각을 하게 됐다. 이번 글에서는 산업 전반에 걸쳐 많은 모델을 적용해본 모범 사례를 다룬다. 이번 글에서 제시하는 모범 사례를 통해 좀 더 일관되고, 재사용 가능하며, 유지 보수성이 좋은 도메인 모델을 이끌어낼 수 있다. 모범 사례의 내용을 증명하는 데 하이버네이트를 사용했지만, 다른 ORM 도구에도 개념적으로 많은 부분을 적용할 수 있다.
이번 글은 2부로 나눠 진행한다. 1부에서는 다음과 같은 기본 개념을 다룬다.
- 도메인의 공통 기능 구현
- 데이터 접근 티어와 중복되는 코드 줄이기
- 엔티티 변경 감시를 일관되게 제어하기
2부에서는 이번 글에서 다룬 개념에 대해 좀 더 깊은 내용을 조명해보고, 도메인 모델에 대한 성능 튜닝에 대해서도 다뤄볼 생각이다.
처음부터 다시: 객체 모델로 시작하기
영속성 객체를 지원하는 객체 모델 정의는 일반 객체 모델 정의와 같은 단계를 거친다. 먼저 모든 객체에서 공유할 공통 엘리먼트를 찾아야 한다. 공통 엘리먼트와 더불어 영속화 객체(애플리케이션 실행 전반에 걸쳐진)를 식별하는 유일한 방법과 객체 인스턴스에 대한 정보의 식별과 같은 영속화 정보에서 필요한 공통 엘리먼트를 찾아내자. 그림 1은 이 두 개념을 인터페이스와 기본 클래스를 사용해 어떻게 정의하는지를 보여준다.
그림 1. 공통 인터페이스와 기본 클래스들
그림 1은 객체 인스턴스를 식별해내고, 객체 인스턴스에 정보 설정을 감시하는 API를 정의하는 Identifiable과 Auditable 인터페이스를 보여준다. 또한 정보 검사를 필요로 하는 객체인지 필요로 하지 않는 객체인지에 따라, 구상 영속성 클래스가 상속할 수 있는 BaseEntity와 AuditableEntity 기본 클래스를 소개하고 있다.
이 인터페이스로 정의된 영속성 객체는 모든 구상 객체 타입에 적용할 수 있는 추상 행동을 생성할 수 있는 높은 기회를 제공한다. 이는 서비스와 데이터 티어뿐만 아니라 CRUD(create, read, update, delete) 오퍼레이션이 일어날 객체를 식별하는 UI 티어도 포함한다. 이번 글의 코드 예제(완전한 페키지를 받으려면 다운로드 참조)는 검사를 쉽게 할 수 있고, 데이터 접근 객체(Data Access Object: DAO)의 코드 중복을 줄이는 인테페이스 사용 방법을 보여준다.
공통 기본 엔티티
객체와 다르게 데이터베이스 테이블은 상속에 대한 개념이 없다. 검사 필드처럼 많은 테이블에서 공통으로 사용하는 필드는 각 테이블마다 반드시 재정의를 해야 한다. 이 점을 고려해 보면, 중복이 코드 전체에 퍼지지 못하도록 자바(Java™) 코드에서는 상속을 사용할 수 있다. 비록 ORM 도구가 오랫동안 이 특성을 지원해 왔지만, 자바 영속성(Java Persistence) API 애노테이션에서는 코드 중복을 훨씬 더 줄일 수 있도록 상속을 더 쉽게 구현할 수 있게 해준다(참고자료 참조).
클래스 수준 애노테이션은 자바 5 애노테이션을 사용해 클래스 소스 코드에 직접 데이터베이스 매핑을 포함한다. 자바 영속성 API는 이러한 목적으로 표준 애노테이션 집합을 정의한다. 이제 하이버네이트와 기타 도구들 또한 표준 애노테이션 지원을 제공한다. @MappedSuperclass 애노테이션은 기본 클래스에 매핑을 정의해 사용할 수 있게 해준다. @MappedSuperclass 애노테이션을 사용하면, 공통 필드에 대해 같은 컬럼 타입과 이름을 사용하는 데이터베이스 테이블을 사용하는 동안에는, 일단 기본 클래스에 매핑을 작성하면 모든 하위 클래스에서 컬럼을 재사용할 수 있다. Listing 1은 BaseEntity 클래스의 예를 보여준다.
Listing 1. @MappedSuperclass로 정의한 BaseEntity
@MappedSuperclass
public class BaseEntity implements Identifiable{
private Long id;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
|
Listing 1에서 작성한 매핑은 기본 컬럼 이름(id)을 ID 필드로 매핑해 주는 효과를 보여주며, (특정 데이터베이스에 구현에 맞춰) ID가 자동으로 생성되도록 지정했다.
심지어 컬럼 이름이 각 테이블마다 달라도 공통 기본 필드를 사용할 수 있다는 것을 기억하자. 대표적인 예로, 모든 데이터베이스 테이블에서 사용하는 주 키가 항상 Long인 경우에도, 컬럼 이름이 달라도 공통 기본 필드를 사용할 수 있다. 특정 하위 클래스에 할당되어 있는 속성을 재정의해 여전히 id 속성으로 공통 코드를 사용할 수 있다. Listing 2는 id 속성과 연계되어 있는 컬럼을 재정의하는 방법을 보여준다.
Listing 2. id 속성과 연계되는 컬럼 재정의하기
@Entity
@AttributeOverride( name="id", column = @Column(name="EMPLOYEE_ID") )
public class Employee extends BaseEntity {//...}
|
하이버네이트 애노테이션을 사용하지 않는 경우에도 코드를 재사용할 수 있다. 그렇지만 각 구상 클래스마다 필드를 매핑해야만 한다. 하이버네이트는 자동으로 자바 코드에서 상속받은 필드를 사용하게 된다.
데이터 접근 핵심
이름이 암시하는 것처럼, DAO 패턴은 객체나 관련된 객체 집합의 데이터에 접근하는 로직을 캡슐화한다. 하이버네이트에서 DAO는 Criteria 쿼리와 Hibernate Query Language 쿼리를 포함하고, 하이버네이트 SessionFactory를 가지고 있다. 모든 데이터베이스 지향 로직을 DAO 내에 포함시켜야 한다는 목적은 POJO(Plain Old Java Object)와 다른 기본형(primitive) 값이 DAO를 통해서만 안팎으로 이동해야 한다는 것을 의미한다. DAO는 엔터프라이즈 자바로 구성되는 아키텍처에서 아주 대표적인 패턴이며, 전형적인 DAO 패턴은 서비스 티어를 통해 접근한다. DAO를 면밀한 조사해보면 많은 DAO의 오퍼레이션이 비슷하다는 점을 발견해낼 수 있다.
먼저 몇 가지 예를 살펴보면서 DAO 사이의 공통점을 찾아 보자. Listing 3에는 그림 1에서 정의한 Identifiable 인터페이스를 ID로 하는 Address와 Employee에 대한 쿼리를 실행하는 메서드 두 개가 나온다.
Listing 3. 데이터 접근 객체의 전형적인 메서드
public Address findById(Long id){
return (Address) getSession().get(Address.class, id);
}
public Employee findById(Long id){
return (Employee) getSession().get(Employee.class, id);
}
|
Employee와 Address에 대한 이 오퍼레이션 사이의 가장 큰 차이점은 단순히 오퍼레이션에서 사용하는 클래스에 있다. 쿼리는 똑같고 결과 또한 다른 클래스로 형 변환(cast)만 하고 있다. 모델에서 해당 엔티티를 삭제하거나 모델에서 엔티티의 모든 인스턴스를 반환하는 것처럼 여타 비슷한 오퍼레이션들은 DAO에서 공통으로 나타나며, 엔티티들 사이에서도 비슷하다. 이런 관점에서, 자바 1.5의 generic 기능을 활용해 데이터 접근 티어의 핵심을 구축할 수 있는 재사용 가능한 DAO를 만들 수 있다.
제네릭 DAO(Generic DAO)
제네릭 DAO 패턴(또는 데이터 타입을 지원하는 DAO(Typesafe DAO)로 부르는)은 데이터 접근 티어의 코드 중복을 줄이는 핵심이다. 자바 1.4를 사용한다면 공통 기본 DAO로 같은 효과를 얻을 수 있다. 공통 기본 DAO 구현은 데이터 타입을 지원하거나 제네릭 DAO처럼 깔끔하지 않지만, 여전히 코드 중복을 제거할 수 있다.
제네릭 DAO를 구현할 수 있는 접근 방법은 환경에 따라 다양하다. 이번 예제에서는 구현에서 어떻게 DAO를 구성하는지에 대한 걱정 없이, 필요한 모든 기능이 사용되기 전에 주입된다고 가정하는 의존성 주입 스타일을 사용한다. 의존성 주입을 사용하지 않는 일부 다른 접근 방법이 있다(더 많은 정보를 얻으려면 하이버네이트 위키에 대한 참고자료 링크를 참조하자). 의존성 주입 접근 방법의 핵심은 쿼리를 실행하는 DAO에 제네릭 타입 정의와 함께 엔티티의 Class를 주입해 데이터를 쿼리하도록 하는 것이다.
제네릭 DAO를 만드는 첫 번째 단계는 공통 오퍼레이션을 정의하는 일이다. 그림 2는 제네릭 DAO를 사용할 수 있는 인터페이스를 보여준다.
그림 2. 기본 DAO 인터페이스와 구현
Listing 4는 제네릭 DAO에 대한 일부 예제 코드를 보여준다.
Listing 4. 제네릭 DAO 패턴에 대한 예제 코드
public interface BaseDao<B extends BaseEntity> {
B getById(Long id);
// ...other methods
}
public class BaseDaoHibernate<B extends BaseEntity>
implements BaseDao<B extends BaseEntity> {
private Class<B> queryClass;
public B getById(Long id) {
return (B) getSession().get(getQueryClass(), id);
}
// ...other methods
}
|
Listing 4에 나오는 메서드는 데이터 접근 티어의 핵심을 이룬다. 이 제네릭 DAO는 직접 사용할 수도 있고, 쿼리를 실행하는 엔티티의 필요에 따라 하위 클래스에 의존해 사용할 수도 있다. 제네릭 DAO는 Listing 5에서 볼 수 있듯이, 서비스를 통해 직접 사용할 수도 있다.
Listing 5. 서비스를 통해 generic DAO 사용하기
BaseDao<Employee> dao = new BaseDao<Employee>();
dao.setQueryClass(Employee.class);
dao.setSessionFactory(sessionFactory);
...
dao.getById(-1L);
|
데이터 집합이 좀 더 복잡해 더 커스터마이징된 쿼리가 필요한 경우, 제네릭 DAO를 상속해 하위 클래스를 만들 수 있다. 예를 들어, lowa에서 살고 있는 정규직 근로자를 모두 찾고 싶다고 가정해보자. 이를 위해 findIowaEmployees란 이름으로 Employee에 적합한 DAO 메서드를 정의하고 싶을지도 모른다. BaseDAO를 상속해 EmployeeDAO를 새로 생성한다면, EmployeeDAO는 Listing 6에서 볼 수 있듯이, 제네릭 DAO에서 제공되는 모든 기본 쿼리와 함께 정규직 근로자를 위한 특정 쿼리도 포함할 수 있다.
Listing 6. 제네릭 DAO의 하위 클래스 만들기
public class EmployeeDao extends BaseDaoImpl<Employee> {
public EmployeeDao() {
setQueryClass(Employee.class);
}
List<Employee> findIowaEmployees() {
Criteria crit = getCurrentSession().createCriteria(getQueryClass());
crit.createCriteria("address").add(Restrictions.eq("state", "IA"));
return crit.list();
}
}
|
Listing 6에서는 createCriteria() 메서드를 사용한다는 점을 주의하자. 이 메서드는 엔터프라이즈 애플리케이션에서 DAO 사이에서 자주 중복이 발생하는 또 다른 메서드다. 우리는 제네릭 DAO를 사용할 수 있기 때문에 재사용성을 향상시키고 중복을 줄이도록 제네릭 DAO에 추가할 수 있는 새로운 공통 오퍼레이션을 찾을 수 있을 것이다. 진정한 페이지 처리와 (true pagination)과 검색 매개변수 다루기 같은 공통 메서드에 대한 다른 예제들은 2부에서 자세하게 다룬다.
검사하기
기초 검사는 데이터베이스 중심 애플리케이션에서 발견할 수 있는 공통적인 특성이다. 대부분 애플리케이션에서는 일부 혹은 모든 객체가 누군가에 의해 생성되고, 또한 누군가에 의해 지속적으로 수정되는 내용을 기록해두기를 원한다. 이러한 특성을 모델링하는 것은 어렵지 않다. 검사를 요구하는 어느 객체든 각각 검사 정보의 일부를 보유하는 추가 필드 네 개만 있으면 된다. 그림 1은 필요로 하는 검사 필드를 포함하는 영속성 엔티티의 기본 클래스를 보여준다. 이 기능에서 좀 더 어려운 부분은 어디에 검사 데이터를 저장하는지에 대해 이해하는 점이다. 몇 가지 선택할 수 있는 조건이 있다.
- 기준 없이 무작위 추가(Brute force): 간단하게 객체가 요구하는 검사 정보가 어디서 수정됐든지 검사 정보를 정확한 값으로 가져왔는지 확인할 수 있다. 이 방법은 분명 약점이 많다. 가장 큰 약점은 검사 로직이 애플리케이션 전체에 걸쳐서 중복된다는 점이다. 심지어 검사 로직을 유틸리티 클래스로 모았더라도 최초 시스템을 개발할 때뿐만 아니라 유지보수할 때도 여전히 객체를 수정해야 하며 언제나 유틸리티 클래스를 사용해야 한다는 점을 기억해야 한다. 상당한 크기의 시스템에 있는 개발자라면 누구라도 언젠가는 이 내용을 잊어버리기 마련이다.
- DAO에 로직 추가: 또 다른 가능성은 공통 DAO에 로직을 집중하는 방법이다. 저장하는 메서드에 검사 로직을 추가해 DAO를 사용해서 저장하는 모든 객체가 자동으로 검사 필드에 값을 채우게 한다. 이 방법은 많은 상황에 적합하지만, 여전히 몇 가지 결함이 남아 있다. 그 중 하나는 데이터를 저장하는 데 항상 공통 DAO의 저장 메서드만 사용될 것이라고 가정해야 한다는 점이다. 항상 이런 경우만 있는 것은 아니므로 검사 로직 추가를 기억해야 할 필요가 있는 이슈가 다시 제기된다. 다른 문제는 더 큰 문제인데, 이 해결책이 ORM의 가장 유용한 특성인 영속성 전이(transitive persistence)를 무시한다는 점이다. DAO는 명시적으로
Employee 객체를 저장할 수 있지만, 하이버네이트는 Employee와 연관을 맺고 있는 Address 객체에서 발생한 모든 변경도 자동으로 영속화해 준다. 이런 경우엔 Employee는 객체의 검사 필드에 값을 채울 수 있지만, Address는 채울 수 없게 된다.
- 하이버네이트
Interceptor: 이러한 문제를 해결을 목적으로 하이버네이트로 구축할 수 있는 확장 지점이 필요하다. 프레임워크에서 객체를 저장할 때마다 검사 필드를 할당(populate)할 필요가 있다. 고맙게도 하이버네이트는 Interceptor 인터페이스로 정확하게 이 기능을 제공한다. Interceptor 인터페이스는 객체 생성, 수정과 삭제를 포함하는 다양한 하이버네이트 이벤트에 대한 콜백 메서드를 제공한다. 하이버네이트 Interceptor로 검사 로직을 구축하면 반복해 사용하는 로직을 제거할 수 있고, 실행 시 로직 확인에 대한 걱정도 할 필요가 없게 된다. 하이버네이트가 데이터를 저장하는 책임을 갖는 한 검사는 항상 일어나게 된다.
자세한 구현 내용
하이버네이트는 Interceptor 인터페이스에서 찾을 수 있는 열 개가 넘는 콜백 메서드의 빈 구현체를 제공하는 EmptyInterceptor 클래스를 가지고 있다. 이 클래스는 검사 정보를 추가하기에 제일 적합하다. Listing 7에서 보여주는 자세한 구현 내용에는 새로운 객체가 데이터베이스에 플러시(flush)할 때마다 호출되는 onSave와 하이버네이트가 데이터베이스에 수정된(또는 변경된(dirty)) 객체를 플러시할 때마다 호출되는 onFlushDirty 등 관련된 메서드 두 개가 나온다.
Listing 7. EmptyInterceptor 상속하기
public class AuditInterceptor extends EmptyInterceptor {
@Override
public boolean onSave(Object entity, Serializable id, Object[] currentState,
String[] propertyNames, Type[] types) {
...
}
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,
Object[] previousState, String[] propertyNames, Type[] types) {
...
}
}
|
두 이벤트 모두 하이버네이트가 객체의 프로퍼티나 프로퍼티의 값 중에서 저장될 것이 있는지 결정한 후에 호출된다. 프로퍼티 이름은 String 배열로, 프로퍼티 값은 Object 배열로 객체에 전달된다. 하이버네이트는 이미 오퍼레이션의 대상이 되는 값을 결정했기 때문에 직접 객체를 갱신하면 원하는 효과를 얻지 못한다. 사실 객체 갱신은 궁극적으로 데이터베이스로 보내게 될 값에 전혀 영향을 미치지 않는다. 대신 프로퍼티-값 배열의 엘리먼트들이 갱신될 필요가 있는 값들이다. 때론 배열을 사용하는 접근 방법이 성가시게 보여도 여전히 구현 내용이 간단하며 매우 직관적이다. 검사 필드를 찾으려면 프로퍼티 이름의 배열을 반복해가면서 찾으면 된다. 필드를 찾은 경우, 해당 인덱스로 프로퍼티의 값을 갖는 배열에서 동일한 인덱스를 갖는 값을 갱신하면 된다. 마지막으로 이러한 콜백 메서드들은 Boolean 값을 반환한다는 점을 자세하게 봐야 한다. 객체 상태가 수정된 경우, 메서드는 true를 반환할 필요가 있다. 아무런 변경이 발생하지 않았을 때는 false를 반환한다. Listing 8에서는 예제 코드에 대한 로직을 보여준다.
Listing 8. Audit 필드를 갱신하는 Interceptor 콜백 메서드
for(int i = 0; i < propertyNames.length; i++) {
if(propertyNames[i].equals("createdOn")) {
currentState[i] = new Date();
updated = true;
}
if(propertyNames[i].equals("createdBy")) {
currentState[i] = username;
updated = true;
}
}
|
마지막 단계는 검사 필드의 이름을 강요하는 것과 데이터베이스 엔티티가 검사 필드를 갖도록 돕는 방법이다. Auditable 인터페이스가 결국 이 목적을 달성하는 데 가장 쉽고 가장 빠른 방법이 되지만 몇 가지 이상한 점을 볼 수 있다. 비록 검사 필드에 대한 getter와 setter 메서드가 제공되지만, 실제로 검사를 진행하는 코드에서는 절대 사용하면 안 된다. 그렇지만 엔티티가 Auditable 인터페이스를 구현함으로써, 영속성 클래스들을 개발할 때 생기는 수 많은 오타를 획기적으로 줄일 수 있다.
1부의 결론
이번 글에서는 하이버네이트 특성을 사용해 기초적인 객체 지향 원칙을 도메인 모델링에 적용하는 데 집중해 봤다. 모든 패턴과 모범 사례는 적절하게 일부 내용을 추가하거나 제거함으로써, 우리가 여기서 계략적으로 살펴본 해결책들을 사용할 영역에 맞춰 평가해봐야 한다. 제네릭 DAO는 이런 유연성(flexibility)의 가장 대표적인 예다. 다양한 데이터베이스 구조, 기술과 비즈니스 요구사항은 제네릭 DAO의 공통 코드로 옮길 수 있는 상이한 공통 기능을 가지고 있다. 다른 검사 정보가 또 다른 애플리케이션에서 필요하게 될지도 모른다(분명한 것은 ATM 기계 도메인 엔티티 트랜잭션에 대한 검사 정보는 직원 주소 검사 정보와는 다르다는 점이다). 이러한 영역이 있지만, 모범 사례는 여전히 변함없이 존재한다.
이번 연재의 2부에서는 하이버네이트로 다형성(polymorphism)을 활용해 데이터 모델을 구축하는 모범 사례와 제네릭 DAO의 다른 유용한 기능, 데이터 모델을 튜닝(tuning)하는 성능 문제에 대해 더 자세하게 다뤄본다.
다운로드 하십시오 | 설명 | 이름 | 크기 | 다운로드 방식 |
|---|
| 이 글의 예제 코드 | PatternsOfPersistenceCode.zip | 16KB | HTTP |
|---|
참고자료 교육
제품 및 기술 얻기
토론
필자소개  | 
|  | Ryan Senior는 Source Allies, Inc.의 컨설턴트로 보험, 금융, 제조, 건강 관리 분야에서 7년의 경험을 가지고 있다. Ryan Senior는 엔터프라이즈 자바 소프트웨어 설계와 개발을 전문으로 한다. |
 | 
|  | Travis Klotz는 Source Allies, Inc.의 컨설턴트다. Travis Klotz는 교육, 보험과 엔지니어링 분야에서 10년간의 소프트웨어 개발 경험이 있다. 오픈 소스 프레임워크를 사용하는 엔터프라이즈 소프트웨어 개발을 전문 분야로 하며, 자동화된 빌드 관리 시스템을 조직이 개발하고 배포할 수 있도록 돕는다. |
 | 
|  | Jim Majure는 Source Allies, Inc.의 컨설턴트다. 과학, 금융과 보험 분야에서 20년 넘게 소프트웨어를 개발한 경험을 가지고 있다. 현재 전문 분야는 엔터프라이즈 자바 소프트웨어 설계와 개발이다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|  |