slipp 서비스를 java에서 scala로 전환하면서 경험담

91
source of slipp.map(java => scala)

Upload: javajigi-jaesung

Post on 21-Jan-2018

2.460 views

Category:

Software


0 download

TRANSCRIPT

source of slipp.map(java => scala)

Scala를선택하기까지의 삽질 경험담

Spring + JPA + Scala 조합을 선택한 이유

SLiPP 서비스를 Java에서 Scala로 전환하는 과정

Scala 전환을 통해 얻는 이점

Spring + JPA + Scala 조합시 고려할 사항

Scala를 선택하기까지의 삽질 경험담

40대를 넘어선 2013년의 어느 날

• 10년 이상 Java만 했더니 재미없다.

• 최근에 함수형 언어가 대세라더라.

• 삶이 무료하다. 뭔가 재미있는 일 좀 있으면 좋겠다.

• 40대가 되면 생긴다는 그 병

일단 시작하는 거 제대로 공부하자.

• Scheme이라는 완전히 생소한 언어에 도전

• 내용 진짜 어려움

• 실무에 적용할 곳이 보이지 않음.

• 서서히 동기부여가 되지 않으면서 중도 포기.

• Scheme이라는 완전히 생소한 언어에 도전

• 내용 진짜 어려움

• 실무에 적용할 곳이 보이지 않음.

• 서서히 동기부여가 되지 않으면서 중도 포기.

하지만 함수형 언어가 무엇인지에 대한 대략적인 감은 잡음

2013년 말 다시 병이 도짐

• 최근에 함수형 언어가 대세라더라.

• 삶이 무료하다. 뭔가 재미있는 일 좀 있으면 좋겠다.

• JVM 기반 함수형 언어를 학습하면

실무 적용 가능할거야.

• Clojure는 Scheme과 문법이

비슷하잖아.

• SLiPP 스터디에 Clojure 스터디 진행

• 스터디가 항상 그렇듯이 초반

3주차까지 좋았음.

• 4주차 이후부터 스터디원이 거의

나오지 않음.

• 스터디 중도 포기. 지금까지 여러

번의 SLiPP 스터디를 진행했지만

중도 포기한 유일한 스터디

• 나도 실무에 적용할 방법을 찾지 못함

하지만 스터디원 중의 한 명은 클로저 빠로 만듦

2014년 ~ 2015년 초반

• 마흔이 넘어 무슨 함수형 언어야.

• 그냥 자바로 15년만 더 버티자.

• 그러던 어느 날 스터디에 Scala를 하잖다.

• 함수형의 극한을 맛보는 경험을 포기하고 현실과 타협하자.

• 나란 인간은 실무에 적용하지 않으면 지속적인 동기부여를

하기 힘들다.

• 욕심을 버리고 Scala로 가자.

• 스스로 자위하면서 스터디에 합류.

• 아직도 함수형 프로그래밍에 대한

욕심을 버리지 못함.

• 책 무지 좋다. 단, Scala 문법도

익숙하지 않은 초보자가 보기 너무

어려움.

• 스터디 초반 교재로 고민하다 포기함.

• 돌고 돌아 함수형 프로그래밍에 대한

욕심을 버리고 Programming in

Scala

• 욕심을 버렸는데 재미가 없다.

Scala도 스터디 끝나면 기억 속에서

사라질 거 같다.

• SLiPP을 Java에서 Scala로 전면

개편하자.

느낀 점

• 2013년 무작정 시작했던 함수형 프로그래밍에 대한 도전이 Scala를

통해 결실을 맺다.

• 너무 많은 것을 고려한 후 실행에 옮기기 보다 무작정 실행해 보는

것도 삶에 새로운 길을 만든다.

Spring + JPA + Scala 조합을 선택

• 돌고 돌아 함수형 프로그래밍에 대한 욕심을 버리고

Programming in Scala

• 욕심을 버렸는데 재미가 없다. Scala도 스터디 끝나면 기억

속에서 사라질 거 같다.

• SLiPP을 Java에서 Scala로 전면 개편하자.

• 돌고 돌아 함수형 프로그래밍에 대한 욕심을 버리고

Programming in Scala

• 욕심을 버렸는데 재미가 없다. Scala도 스터디 끝나면 기억

속에서 사라질 거 같다.

• SLiPP을 Java에서 Scala로 전면 개편하자.

Play가 아닌 Scala + Spring + JPA 조합을 선택한

이유

• SLiPP 서비스를 처음부터 다시 구현하는 것에 대한 부담감

• Spring과 JPA 경험을 버리는 것의 아쉬움.

• 중도 포기하지 않으려면 변화를 Scala 하나로 한정.

“나름 생산성 높다.”

라는 자기 합리화

귀찮다.

마흔이 넘어보면 알 수 있다.

source of slipp.map(java => scala)

2015년 5월 22일 시작

2015년 12월 30일 끝

Scala 적용(리팩토링) 원칙

• 기존 기능을 서비스하면서 점진적으로 리팩토링한다.

• 리팩토링 단계에 컴파일 에러가 발생하는 시간을 최소화한다.

1단계 – Java와 Scala 동시 개발 환경구축

• Java와 Scala 코드가 동시에 컴파일과 실행이 가능하도록 환경

구축함.

• Java 소스 코드

• src/main/java, src/test/java

• Scala 소스 코드

• src/main/scala, src/test/scala

<dependency><groupId>org.scala-lang</groupId><artifactId>scala-library</artifactId><version>${scala.version}</version>

</dependency>

<plugin><groupId>net.alchim31.maven</groupId><artifactId>scala-maven-plugin</artifactId><version>3.2.1</version><configuration>

<recompileMode>incremental</recompileMode><javacArgs>

<javacArg>-Xlint:unchecked</javacArg><javacArg>-Xlint:deprecation</javacArg>

</javacArgs><source>${jdk.compiler.version}</source><target>${jdk.compiler.version}</target>

</configuration><executions>

<execution><id>scala-test-compile</id><phase>test-compile</phase><goals>

<goal>testCompile</goal></goals>

</execution><execution>

<phase>process-resources</phase><goals>

<goal>compile</goal></goals>

</execution></executions>

</plugin>

• 10년 이상 이클립스 기반으로

살아왔다.

• 이클립스에서 Scala IDE 활용해

Scala 개발 시작하는 것은 당연한

수순이었다.

• 이클립스 버리고 싶지 않았는데 몇 달

후 GG

• 이클립스 버리고 싶지 않았다.

• 하지만 Scala는 Intellij의 Scala

플러그인이 갑이다.

• Community 버전에서도 무료로 사용

가능하다.

2단계 – 위험 요소가 높은 부분부터실험

• Java 기반의 SLiPP 코드는 Spring + JPA(Spring Data JPA) 구조

• JPA 기반 Entity를 Scala로 개발 가능한지 실험

• 다른 Entity와 가장 의존관계가 적은 기능을 Scala로 먼저 변경

• JPA Entity 실험과 리팩토링 원칙에 대한 경험을 같이 진행

package net.slipp.domain.tag;

[...]

@Entity@Table(indexes = {

@Index(name = "idx_tagged_history_tag", columnList="tag_id"),@Index(name = "idx_tagged_history_question", columnList="question_id")}

)@EntityListeners({ CreatedDateEntityListener.class })public class TaggedHistory implements HasCreatedDate {

@Id@GeneratedValue(strategy = GenerationType.AUTO)private Long historyId;

@Column(name = "tag_id", nullable = false, updatable = false)private Long tagId;

@Enumerated(EnumType.STRING)@Column(name = "tagged_type", nullable = false, updatable = false, columnDefinition =

TaggedType.COLUMN_DEFINITION)private TaggedType taggedType;

public TaggedHistory(Long tagId, Long questionId, Long userId, TaggedType taggedType) {this.tagId = tagId;this.questionId = questionId;this.userId = userId;this.taggedType = taggedType;

}

[...]}

Scala 적용(리팩토링) 원칙

• 기존 기능을 서비스하면서 점진적으로 리팩토링한다.

• 리팩토링 단계에 컴파일 에러가 발생하는 시간을 최소화한다.

자바 리팩토링 과정과 같은 방식으로

Java => Scala 전환 과정 설계

public abstract class JdbcTemplate {public void update() throws SQLException {

[…]}

public abstract String createQuery();

public abstract void setValues(PreparedStatement pstmt) throws SQLException;}

리팩토링 전

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;}

리팩토링 중(과도기 단계)

public abstract class JdbcTemplate {public void update(String sql) throws SQLException {

[…]}

public abstract void setValues(PreparedStatement pstmt) throws SQLException;}

리팩토링 후

Scala 적용(리팩토링) 원칙

• 기존 기능을 서비스하면서 점진적으로 리팩토링한다.

• 리팩토링 단계에 컴파일 에러가 발생하는 시간을 최소화한다.

• 리팩토링 전과 후의 코드가 공존하는 단계가 반드시 필요하다.

• 이 같은 전략은 소스 코드 리팩토링 뿐 아니라 Java => Scala 전환, DB 리팩토링 또한 같다.

• 리팩토링 전과 후의 결과를 쉽게 테스트할 수 있어야 한다.

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

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)}

매핑 중 삽질 내용 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)}

매핑 중 삽질 내용 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 내부에서

매핑을 위한 변환 작업이 필요하다.

실험을 통한 결론 및 얻게 된 경험• Scala 기반으로 JPA 기반 개발 가능.

• Scala => Java API 접근은 문제 없음. But, Java => Scala API 접근에 한계가 많음을 느낌.

• Controller => Service => Repository + Entity 순서로 리팩토링 전략 수립함.

3단계 – Layer별로 Scala 전환

• Controller부터 Scala로 리팩토링 시작

• 리팩토링 과정

• src/main/scala의 같은 package에 NHomeController.scala를 생성

• NHomeController로 URL 하나씩 이동하면서 관련된 method 이전함.

• 컴파일 에러가 없는 상태로 만든 후 HomeController.java에서 Controller Annotation 제거함.

• 웹 서버 시작해 기능이 정상적으로 동작하는지 테스트

• 정상 동작을 확인하면 HomeController.java를 제거함.

• NHomeController 파일을 HomeController로 rename 리팩토링 진행함.

• 모든 Controller에 대해 무한 반복

지루하고 짜증나는 반복 과정의 연속

속도가 잘 나지 않는다.

방법 1.

IntelliJ의 신세계 경험

Java 소스 코드를 Scala 소스 코드로 자동 변환해 준다.

방법 2.

일단 모든 Java 코드를 Scala 코드로 전환하는 것을 1차 목표로 한다.

Scala 코드로 전환하는 과정에서 절대 리팩토링을 하지 않는다.

토할 정도로 Java 코드를 Scala 코드로 전환하는 것에만 집중.

4단계(ing) –Scala 스타일로 리팩토링

private String sendMessageToFacebook(SocialUser loginUser, String link, String receiverId, String message) {

String postId = null;try {

FacebookClient facebookClient = createFacebookClient(loginUser);int i = 0;do {

if (i > 2) {break;

}

FacebookType response = facebookClient.publish(receiverId + "/feed", FacebookType.class,Parameter.with("link", link), Parameter.with("message", message));

postId = response.getId();

i++;} while (postId == null);log.debug("connect post id : {}", postId);

} catch (Throwable e) {log.error("Facebook Connection Failed : {}", e.getMessage());

}return postId;

}

리팩토링 사례 1 – java 코드

@annotation.tailrecprivate def retry[T](n: Int)(fn: => T): T = {Try { fn } match {case Success(x) => xcase _ if n > 1 => retry(n - 1)(fn)case Failure(e) => throw e

}}

리팩토링 사례 1 – scala 코드

private def sendMessageToFacebook(loginUser: SocialUser, link: String, receiverId: String, message: String) = {retry(3) {val facebookClient: FacebookClient = createFacebookClient(loginUser)val response: FacebookType = facebookClient.publish(receiverId + "/feed",

classOf[FacebookType], Parameter.`with`("link", link), Parameter.`with`("message", message))

response.getId}

}

public Set<Tag> processGroupTags(Set<FacebookGroup> groupTags) {Set<Tag> tags = Sets.newHashSet();for (FacebookGroup each : groupTags) {

if (each.isEmpty()) {continue;

}

Tag tag = tagRepository.findByGroupId(each.getGroupId());if (tag != null) {

tags.add(tag);continue;

}

tag = tagRepository.findByName(each.getName());if (tag != null) {

tag.moveGroupTag(each.getGroupId());tags.add(tag);continue;

}

Tag newTag = Tag.groupedTag(each.getName(), each.getGroupId());tags.add(tagRepository.save(newTag));

}return tags;

}

리팩토링 사례 2 – java 코드

Option(tagRepository.findByGroupId(facebookGroup.getGroupId)).orElse(Option(tagRepository.findByName(facebookGroup.getName)).map(t => t.moveGroupTag(facebookGroup.getGroupId)))

.getOrElse(tagRepository.save(Tag.groupedTag(facebookGroup.getName, facebookGroup.getGroupId)))

리팩토링 사례 2 – scala 코드

지금까지 내용 정리

Java에서 Scala 전환 단계

• 1단계 : Java와 Scala를 같이 실행할 수 있는 환경을 구축한다.

• 2단계 : Scala 전환시 위험요소가 있다고 판단되는 부분이 있다면

이에 대한 실험을 먼저 진행하고 전략을 세운다.

• 3단계 : 앞에서 세운 전략에 따라 Scala 전환 작업을 진행한다.

• 4단계 : Scala 스타일로 리팩토링한다.

Scala를 통해 얻는 이점

1. Domain과 DTO의 명확한 분리에 대한 거부감이

줄어듦

• 현재 개발 추세는 Domain 객체와 DTO에 중복되는 부분이 많아 자바

객체 하나가 Domain 역할, DTO 역할을 하는 방식으로 구현.

• Scala를 활용하면 각 역할별로 구현하는 것에 대한 거부감이 줄어듦

@Entityclass User(pEmail: String, pNickName: String, pPassword: String) extends DomainModel {@Id@GeneratedValuevar 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를 생성하지 않아도 된다.

@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 추가함.

class UserForm {@BeanProperty@Emailvar 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 자동

추가

Domain과 DTO의 명확한 분리에 대한 거부감이

줄어듦

• 분리하는 것이 항상 좋은 것은 아니다.

• 상황에 따라 Domain과 DTO를 분리/통합할 것인지에 대한

역량을 키우는 것이 더 중요하다.

2. Test Fixture(Test Data) 생성하기 용이함.

• 자바에서 Test Fixture를 생성하고 변경하기 어려움은 Test 코드를

만드는데 약간의 장애물이다.

• Scala는 named parameter를 통해 해결 가능

email, nickname, password를 가지는 User 객체에 대한 테스트를 데이터를만든다면…

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);

}}

public class UserTest {@Testpublic void canCreate() throws Exception {

User user1 = new UserBuilder().withEmail("[email protected]").build();User user2 = new

UserBuilder().withEmail("[email protected]").withNickname("newname").build();}

}

22장. 복잡한 테스트 데이터 만들기 참고

trait Fixture {def aSomeUser(email: String = "[email protected]", nickname: String = "nickName", password:

String = "password")= new User(email, nickname, password)

}

val user1 = aSomeUserval user2 = aSomeUser(email="[email protected]")val user1 = aSomeUser(nickName="newname")

• Case Class

• Named Parameter

• Implicit

• Some/Option

• Lambda(람다)

• 막강 Collection

• 모나드 등등 …

• Functional Programming

• Case Class

• Named Parameter

• Implicit

• Some/Option

3. 삶의 무료함을 치유

• 끊임없는 리팩토링 경험

• 나름 생산성 높음

• 객체 지향과 함수형 프로그래밍의 절묘한 조화

• 시대에 뒤쳐지지 않는 듯한 나만의 착각

4. 덤으로 swift 학습

• Scala를 사용했더니 swift 프로그래밍이 껌이다.

Scala 및 Scala + Spring + JPA 기반으로

개발할 때 고려할 부분

Scala + Spring + JPA 조합에서 불편한 부분

• Java 기반 프레임워크 사용하면서 고려할 부분이 생긴다.

• Scala의 모든 기능을 극한으로 사용하는데 제약 사항이 있다.

@RestControllerclass UserController @Autowired() (val userRepository: UserRepository) {val Log = LoggerFactory.getLogger(classOf[UserController])

@RequestMapping(value = Array("/users"), method = Array(RequestMethod.POST) )def join(@Valid @RequestBody userForm: UserForm, result: BindingResult) = {...

}

@RequestMapping(value = Array("/users/{userEmail}"), method = Array(RequestMethod.POST) )def login(@PathVariable userEmail: String, @RequestParam(required = true)

password:String) = {...

}}

import java.lang.Long

import org.springframework.data.repository.CrudRepository

trait UserRepository extends CrudRepository[User, Long] {def findByEmail(email: String): Userdef findByEmailAndPassword(email: String, password: String): Userdef findByNickName(nickName: String): User

}

import java.util.{ArrayList, List}

@Entityclass Clan(pName: String) extends DomainModel {...

@OneToMany(mappedBy = "clan", cascade = Array(CascadeType.PERSIST, CascadeType.REMOVE), fetch = FetchType.LAZY)val clanMembers: List[ClanMember] = new ArrayList[ClanMember]

}

Scala 컴파일 시간

• Scala의 가장 큰 단점은 컴파일 시간이 많이 소요된다.

• Scala 기반으로 개발하려면 SSD는 필수. 장비빨이 받쳐주어야 한다.

Scala 학습 비용이 높다.

• 한 번에 Scala의 모든 기능을 활용하려고 하기 때문이다.

Scala 적용 단계

1. Scala를 자바처럼 구현한다.

2. 점차 Scala 문법에 친숙해지면 Scala 기능을 하나씩 적용한다.

3. 함수형 프로그래밍 스타일로 구현한다.

마치며…

Java만 너무 오래 했더니 재미없다. 새로운 언어에 도전해 보고 싶다.

그런데 그 동안 쌓아온 경험을 버리기는 아깝다.

Scala가 대안이 될 수 있다.

최근 함수형 프로그래밍이 뜬다더라. 새로운 언어를 도입해 함수형

프로그래밍을 학습하고 싶지만 현재 운영 중인 시스템이 Java 환경이다.

Scala가 대안이 될 수 있다.

프로젝트를 새로 시작한다면 play

framework도 대안

대부분의 개발자가 Scala에 익숙하지

않고, Spring 프레임워크에 친숙하다면

Scala + Spring 조합도 괜찮은 조합이

대안이 될 수 있다.

자바 프로그래머로 15년 이상 살아온 나에게는 2016년 현재

Spring-Boot, JPA, Scala가 최선의 조합이고, 흥미로운 조합이라 생각

하지만 Swift로 백엔드 프로그래밍이 가능하다면

1년 후에는 자바 생태계를 떠날지도…