SLiPP(https://slipp.net) 서비스를 Java에서 Scala로 전환하는 과정에 대해 살펴본다. Scala를 선택한 이유, Java와 Scala를 동시에 서비스하면서 점진적으로 리팩토링하는 과정, Scala + Spring 기반으로 개발할 때 고려사항, Java에서 Scala로 전환하는 과정에 대한 경험담을 공유한다.
Slide for talk "Effective Scala" at SpringCamp 2013 explaining how to use Scala in Scala (or more broadly functional language) style. This talk is for Scala novices who are usually ignorant of functional language paradigm but familiar with OOP.
Scala, Spring-Boot, JPA를 활용한 웹 애플리케이션 개발 과정에 대해 다룬다. Spring-Boot와 JPA 조합만으로도 생산성 있는 웹 애플리케이션 개발이 가능하다. 이 조합만으로도 충분히 의미가 있지만 여기에 Scala라는 약간은 불편한 듯 보이는 언어를 도입함으로써 얻을 수 있는 즐거움을 공유한다. Spring-Boot + JPA 조합에 Scala를 적용하면서의 좌충우돌 경험담을 전한다.
Slide for talk "Effective Scala" at SpringCamp 2013 explaining how to use Scala in Scala (or more broadly functional language) style. This talk is for Scala novices who are usually ignorant of functional language paradigm but familiar with OOP.
Scala, Spring-Boot, JPA를 활용한 웹 애플리케이션 개발 과정에 대해 다룬다. Spring-Boot와 JPA 조합만으로도 생산성 있는 웹 애플리케이션 개발이 가능하다. 이 조합만으로도 충분히 의미가 있지만 여기에 Scala라는 약간은 불편한 듯 보이는 언어를 도입함으로써 얻을 수 있는 즐거움을 공유한다. Spring-Boot + JPA 조합에 Scala를 적용하면서의 좌충우돌 경험담을 전한다.
[Spring Camp 2013] Java Configuration 없인 못살아!Arawn Park
Spring Camp 2013 / Track B Session 2
Java Configuration은 Spring 3.0과 함께 등장했습니다. 초기에는 '이게 뭐야?' 싶은 정도로 제대로된 모습을 갖춘 상태가 아니었습니다. 뒤돌아보면 스프링 1.0 시절의 XML을 보는것 같았지요. (웃음)
하지만 3.1이 발표되며 상황이 바뀌었습니다. XML 설정을 대체할 정도로 성장했을 뿐만 아니라 더 많은 것들을 할 수 있게 되었거든요.
이 시간에는 Spring을 사용하는 대표적인 예제 PetClinic(https://github.com/arawn/spring-petclinic)을 Java Configuration으로 재구성한 모습을 코드로 보여드립니다. 그리고 제가 보는 Java Configuration의 매력요소를 공유합니다.
35. • Java 기반의 SLiPP 코드는 Spring + JPA(Spring Data JPA) 구조
• JPA 기반 Entity를 Scala로 개발 가능한지 실험
• 다른 Entity와 가장 의존관계가 적은 기능을 Scala로 먼저 변경
• JPA Entity 실험과 리팩토링 원칙에 대한 경험을 같이 진행
39. public abstract class JdbcTemplate {
public void update() throws SQLException {
[…]
}
public abstract String createQuery();
public abstract void setValues(PreparedStatement pstmt) throws SQLException;
}
리팩토링 전
40. public abstract class JdbcTemplate {
public void update() throws SQLException {
[…]
}
public void update(String sql) throws SQLException {
[…]
}
public abstract String createQuery();
public abstract void setValues(PreparedStatement pstmt) throws SQLException;
}
리팩토링 중(과도기 단계)
41. public abstract class JdbcTemplate {
public void update(String sql) throws SQLException {
[…]
}
public abstract void setValues(PreparedStatement pstmt) throws SQLException;
}
리팩토링 후
42. Scala 적용(리팩토링) 원칙
• 기존 기능을 서비스하면서 점진적으로 리팩토링한다.
• 리팩토링 단계에 컴파일 에러가 발생하는 시간을 최소화한다.
• 리팩토링 전과 후의 코드가 공존하는 단계가 반드시 필요하다.
• 이 같은 전략은 소스 코드 리팩토링 뿐 아니라 Java => Scala 전환, DB 리팩토링 또한 같다.
• 리팩토링 전과 후의 결과를 쉽게 테스트할 수 있어야 한다.
43. Java => Scala 전환 과정 설계(예, TaggedHistory)
• TaggedHistory.java => NTaggedHistory.scala
• Scala로 변경한 코드에서 생성된 Table Schema와 기존 자바 코드에서 생성된 Table Schema가
같은지 검증한다. Table Schema가 같은 시점을 Entity 변환 완료 시점으로 가정한다.
• Table Schema를 검증하기 위한 테스트 도구가 필요.
• TaggedHistory를 사용하는 코드를 NTaggedHistory를 사용하도록 변경한다.
• 테스트한다.
• TaggedHistory Entity를 삭제한다.
• NTaggedHistory.scala => TaggedHistory.scala로 rename
44. package net.slipp.ndomain.tag
[...]
@Entity(name="TaggedHistory")
@Table(indexes = Array(
new Index(name = "idx_tagged_history_tag", columnList="tag_id"),
new Index(name = "idx_tagged_history_question", columnList="question_id")))
class NTaggedHistory(t: Long, q: Long, u: Long, tType: String) extends DomainModel with NHasCreatedDate
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var historyId: Long = _
@Column(name = "tag_id", nullable = false, updatable = false)
val tagId = t
@Column(name = "tagged_type", nullable = false, updatable = false, columnDefinition =
NTaggedType.ColumnDefinition)
val taggedType = tType
def this() = this(0L, 0L, 0L, null)
}
45. 매핑 중 삽질 내용 1
• Scala Annotation에서 배열을 사용하는 경우 기존 Java Annotation 배열({})을 사용할 수 없었다.
Scala Annotation 배열은 Array
package net.slipp.ndomain.tag
[...]
@Entity(name="TaggedHistory")
@Table(indexes = Array(
new Index(name = "idx_tagged_history_tag", columnList="tag_id"),
new Index(name = "idx_tagged_history_question", columnList="question_id")))
class NTaggedHistory(t: Long, q: Long, u: Long, tType: String) extends DomainModel with NHasCreatedDate
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var historyId: Long = _
@Column(name = "tag_id", nullable = false, updatable = false)
val tagId = t
@Column(name = "tagged_type", nullable = false, updatable = false, columnDefinition =
NTaggedType.ColumnDefinition)
val taggedType = tType
def this() = this(0L, 0L, 0L, null)
}
46. 매핑 중 삽질 내용 2
• 매핑 과정 중 해결하지 못한 문제는 java enum을 활용해 Mapping하는 부분이다. Scala에서도
Enumeration을 사용하는 Enum이 있지만 java의 enum과는 동작방식이 달라 그대로 사용할 수
없다.
• 1차 해결 방법은 Scala Entity에서 java enum을 사용하도록 한다. Scala 코드와 Java 코드가
섞여서 사용되는 구조가 된다.
• 2차 해결 방법은 Scala Enum 또는 Scala Case Object를 활용해 해결해야 한다. 이 경우
Entity를 사용하는 Java 코드에서 Scala 클래스에 접근하지 못하는 이슈와 Entity 내부에서
매핑을 위한 변환 작업이 필요하다.
47. 실험을 통한 결론 및 얻게 된 경험
• Scala 기반으로 JPA 기반 개발 가능.
• Scala => Java API 접근은 문제 없음. But, Java => Scala API 접근에 한계가 많음을 느낌.
• Controller => Service => Repository + Entity 순서로 리팩토링 전략 수립함.
49. • Controller부터 Scala로 리팩토링 시작
• 리팩토링 과정
• src/main/scala의 같은 package에 NHomeController.scala를 생성
• NHomeController로 URL 하나씩 이동하면서 관련된 method 이전함.
• 컴파일 에러가 없는 상태로 만든 후 HomeController.java에서 Controller Annotation 제거함.
• 웹 서버 시작해 기능이 정상적으로 동작하는지 테스트
• 정상 동작을 확인하면 HomeController.java를 제거함.
• NHomeController 파일을 HomeController로 rename 리팩토링 진행함.
• 모든 Controller에 대해 무한 반복
61. Java에서 Scala 전환 단계
• 1단계 : Java와 Scala를 같이 실행할 수 있는 환경을 구축한다.
• 2단계 : Scala 전환시 위험요소가 있다고 판단되는 부분이 있다면
이에 대한 실험을 먼저 진행하고 전략을 세운다.
• 3단계 : 앞에서 세운 전략에 따라 Scala 전환 작업을 진행한다.
• 4단계 : Scala 스타일로 리팩토링한다.
63. 1. Domain과 DTO의 명확한 분리에 대한 거부감이
줄어듦
• 현재 개발 추세는 Domain 객체와 DTO에 중복되는 부분이 많아 자바
객체 하나가 Domain 역할, DTO 역할을 하는 방식으로 구현.
• Scala를 활용하면 각 역할별로 구현하는 것에 대한 거부감이 줄어듦
64. @Entity
class User(pEmail: String, pNickName: String, pPassword: String) extends
DomainModel {
@Id
@GeneratedValue
var id: Long = _
@Column(unique = true, nullable = false)
val email = pEmail
@Column(name = "nick_name", nullable = false)
val nickName = pNickName
@Column(nullable = false)
val password = pPassword
def isGuest(): Boolean = {
false
}
}
User Entity
• 반드시 setter/getter를 생성하지 않아도 된다.
65. @JsonIgnoreProperties(ignoreUnknown = true)
@JsonPropertyOrder(alphabetic = true)
@JsonInclude(Include.NON_NULL)
@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility =
Visibility.NONE, setterVisibility = Visibility.NONE)
trait View
case class UserView(id: Long, email: String, nickName: String) extends
View {
def this(u: User) = this(u.id, u.email, u.nickName)
def this() = this(new User())
}
User View DTO
• Scala case class를 활용하면 자동으로 field 추가함.
66. class UserForm {
@BeanProperty
@Email
var email: String = _
@BeanProperty
@NotNull
@Size(min = 3, max = 10)
var nickName: String = _
@BeanProperty
@NotNull
@Size(min = 8, max = 15)
var password: String = _
def toUser() = new User(email, nickName,
password)
}
User Form DTO
• @BeanProperty 활용하면 setter/getter method 자동
추가
67. Domain과 DTO의 명확한 분리에 대한 거부감이
줄어듦
• 분리하는 것이 항상 좋은 것은 아니다.
• 상황에 따라 Domain과 DTO를 분리/통합할 것인지에 대한
역량을 키우는 것이 더 중요하다.
68. 2. Test Fixture(Test Data) 생성하기 용이함.
• 자바에서 Test Fixture를 생성하고 변경하기 어려움은 Test 코드를
만드는데 약간의 장애물이다.
• Scala는 named parameter를 통해 해결 가능
70. public class UserBuilder {
private String email;
private String nickname;
private String password;
public UserBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserBuilder withNickname(String nickname) {
this.nickname = nickname;
return this;
}
public UserBuilder withPassword(String password) {
this.password = password;
return this;
}
public User build() {
return new User(email, nickname, password);
}
}
71. public class UserTest {
@Test
public void canCreate() throws Exception {
User user1 = new UserBuilder().withEmail("some@sample.com").build();
User user2 = new
UserBuilder().withEmail("some@sample.com").withNickname("newname").build();
}
}
22장. 복잡한 테스트 데이터 만들기 참고
72. trait Fixture {
def aSomeUser(email: String = "some@sample.com", nickname: String = "nickName", password:
String = "password")
= new User(email, nickname, password)
}
val user1 = aSomeUser
val user2 = aSomeUser(email="some2@sample.com")
val user1 = aSomeUser(nickName="newname")