SLiPP(https://slipp.net) 서비스를 Java에서 Scala로 전환하는 과정에 대해 살펴본다. Scala를 선택한 이유, Java와 Scala를 동시에 서비스하면서 점진적으로 리팩토링하는 과정, Scala + Spring 기반으로 개발할 때 고려사항, Java에서 Scala로 전환하는 과정에 대한 경험담을 공유한다.
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")