블로그 이미지
App 개발에 대한 내용을 다룰 예정입니다. AppleSherbet

카테고리

분류 전체보기 (39)
한국프로야구 어플 (18)
세미나 (2)
Java Development (13)
Android App소개 (3)
기타 (2)
당근마켓 (1)
Total297,962
Today12
Yesterday16
Play2 A first iteration of the data model

주말에 열심히 노느라 2탄을 이제야 하게된다.
자~ 이제 우리는 처음으로 우리의 YABE 블로그에 Model 을 하나 추가하겠다.

JPA 소개
Model 레이어는 대부분의 잘 디자인 된 어플리케이션의 가장 중심부에 위치한다. 그만큼 중요한 역할을 하고 있다.
우리가 만들려고하는 어플리케이션이 블로그인 만큼 User, Post 그리고 Comment 등등의 모델 클래스들을 만들게 될 것이다.
Play가 자바 기반인 만큼 우린 database를 직접 엑세스 하는 방식이 아닌
ORM(Object Relational Mapper) 방식을 사용하여 엑세스 할 것이다.
ORM을 이용하면 예측하기 힘든 실수를 미연에 방지할 수 있다.
Java Persistence API(JPA)는 ORM(Object Relational Mapper) 을 정의해놓은 자바 스팩 이다. 그리고 Play 에서는 JPA를 사용하여 만들어진 Hibernate 를 이용해 DB를 엑세스 한다. 만약 JPA나 Hibernate에 대한 사전 지식이 없다해도 이번 장을 이해하는데 전혀 어려움이 없을 것이다. (혹시 이런 기술에 대해서 전혀 처음 들었다면 wikipedia 등을 한번 참고 하는 것도 도움이 될 것이다.)
** ORM: 데이터베이스 연계처리를 위하여 기존의 SQL에 의존하는 것이 아니라, 직접 테이블의 컬럼을 자바 Class에 매핑하거나XML형태의 SQL을 실행하여 처리를 수행하는 Persistence Layer를 담당하는 Framework 개발모델

시작하면서 부터 머리아프니 이제 ORM, JPA, Hibernate 이런건 잠시 잊도록 하자. 우리는 Database 엑세스 또한 순수하게 자바코드상에서 할 것이다. JPA에 대해 궁금하다면 이곳에서~

The User class

우리 블로그는 User 클래스를 만드는 것으로 부터 시작해보자. /yabe/app/models/User.java 를 만들고 아래와 같이 구현한다.
package models;
import java.util.*;
import javax.persistence.*;
import play.db.jpa.*;

@Entity
public class User extends Model {

    public String email;
    public String password;
    public String fullname;
    public boolean isAdmin;

    public User(String email, String password, String fullname) {
        this.email = email;
        this.password = password;
        this.fullname = fullname;
    }
}
User 클래스에 명시된 @Entity 어노테이션과 상속받고 있는 Model 클래스를 주목해서 보자.
@Entity 어노테이션은 이 클래스가 JPA 엔티티라는 것을 표시하는 것이다. 그리고 Model 클래스는 우리에게 유용한 JPA Helper 를 제공해준다. 선언된 모든 field 는 자동으로 database의 field로 맵핑된다. 우리는 각각의 필드에 대한 setter 와 getter를 만들필요가 없음을 유의하자.

[여기서 잠깐] 
1.위와 같이 구현하면 기본동작으로 DB 테이블 이름은 User 가 된다. 만약 테이블 이름을 바꾸고 싶으면 @Table(name="blog_user") 와 같은 어노테이션을 User 클래스에 붙여주면 된다. 
2. 반드시 Model 클래스를 상속 받아야 하는 것은 아니다 다만, 상속 받는 것이 빠르고 쉬운 개발을 가능하게 해준다.(많은 JPA 헬퍼를 이용)

  JPA를 기존에 사용해본 사람이라면 JPA가 @id 프로퍼티를 필요로 한다는 것을 알고 있을 것이다. 하지만 Play에서는 Model 클래스가 자동으로 생성된 id 값을 제공한다. 자바 개발자의 경우 위의 코드에 모든 field가 public으로 선언된 것을 의아하게 생각할지도 모른다. 일반적인 자바 클래스에서는 인스턴스 변수의 경우 private으로 선언하고 getter와 setter를 만들기 때문이다. 하지만 Play의 모델에선 이와 같은 부분을 알아서 처리해주며 자동으로 getter와 setter메소드 또한 생성해 준다.
 
  이제 브라우져로 돌아가서 refresh를 눌러보자. 잘 못 구현한 부분이 없다면 아무일도 일어나지 않을 것이다. 하지만 우리는 단지 Model클래스만 생성한 것이므로 어플리케이션에 별다른 변화는 없다.

  첫번째 테스트 만들기 User 클래스를 만들었으니 Test 코드를 짜기에 적절한 때가 왔다. 테스트를 하기 위해 잠시 서버를 종료하고 (Ctrl + c) 다음과 같이 입력하자.

Play test


Play test 명령어는 Play run 명령어와 거의 동일하다. 다만, test 가 가능하게 하는 test runner 모듈을 함께 로딩한다. Test 모드로 Play를 시작하면 Play는 framework ID를 자동으로 %test 로 바꾼고 해당 ID를 이용해 application.conf 파일을 로딩한다.
** framework ID 에 대해서는 이곳을 참고하자. (아주 간단하므로 꼭 읽어보자)

이제 브라우져를 열고 URL에 http://localhost:9000/@tests 라고 입력 해보자.
기본 테스트를 선택하고 테스트 시작 버튼을 눌러보자. 모든 테스트가 성공할 것이며 성공한 테스트는 초록색 불이 들어온다.(실제로 이 테스트는 어떠한 테스트도 수행하지 않는다. 비어있으므로) 


모델을 테스트 하기위해 JUnit test 를 사용한다. /yabe/test/ 폴더에 보면 이미 BasicTests.java 파일이 존재한다. 파일을 열어보자
import org.junit.*;
import play.test.*;
import models.*;
 
public class BasicTest extends UnitTest {
    @Test
    public void aVeryImportantThingToTest() {
        assertEquals(2, 1 + 1);
    }
}

이제 불필요한 aVeryImportantThingToTest()란 메소드를 지우고 새 메소드를 만들자. 우리가 만들 새 테스트는 새로운 유저를 만들고 데이터 베이스에 저장해서 다시 추출하는 테스트이다.
@Test
public void createAndRetrieveUser() {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save();
    
    // Retrieve the user with e-mail address bob@gmail.com
    User bob = User.find("byEmail", "bob@gmail.com").first();
    
    // Test 
    assertNotNull(bob);
    assertEquals("Bob", bob.fullname);
}
위 코드에서 볼 수 있듯이 Model 클래스는 우리에게 아주 유용한 메소드를 제공한다: save() find(). (끊임 없이 사용하게 될 메소드)
[여기서 잠깐]
Play에서 제공하는 더 많은 메소드를 구경하고 싶다면 play JPA support chapter를 참고하라
 
  다시 브라우져로 돌아가 refresh 를 누르고 BasicTest를 선택한 후 Start 버튼을 눌러보자. 실수가 없었다면 모두 초록색 불이 들어올 것이다.   한가지 테스트를 더 추가하자. 이번 테스트는 특정 이름과 비밀번호를 가진 유저가 있는지 체크하는 테스트이다.
User.java 를 열고 connect() 메소드를 추가하자.
public static User connect(String email, String password) {
    return find("byEmailAndPassword", email, password).first();
}
그리고 다시 /yabe/test/BasicTests.java 파일에 tryConnectAsUSer() 함수를 추가하자.
@Test
public void tryConnectAsUser() {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save();
    
    // Test 
    assertNotNull(User.connect("bob@gmail.com", "secret"));
    assertNull(User.connect("bob@gmail.com", "badpassword"));
    assertNull(User.connect("tom@gmail.com", "secret"));
}
다시 브라우져로 테스트를 돌려보자 :)

Post 클래스

Post 클래스는 블로그의 하나의 Post를 표현하는 Entity 객체이다. /yabe/app/models/Post.java 를 만들고 아래와 같이 구현한다.
package models;
 
import java.util.*;
import javax.persistence.*;
import play.db.jpa.*;
 
@Entity
public class Post extends Model {
 
    public String title;
    public Date postedAt;
    
    @Lob
    public String content;
    
    @ManyToOne
    public User author;
    
    public Post(User author, String title, String content) {
        this.author = author;
        this.title = title;
        this.content = content;
        this.postedAt = new Date();
    }
} 

우리의 위 코드에서 긴 글(여기서는 블로그 포스트의 내용)을 large text database에 저장하기 위해 @Lob이라는 JPA 어노테이션을 사용한다. 그리고 User 클래스와의 Relation을 표현하기 위해 @ManyToOne 어노테이션을 사용한다. 이것은 여러 블로그 포스트가 한명의 유저의 포스트가 될 수 있음을 의미한다. 혹시 이부분이 이해가 되지 않는다면 관계형 데이터베이스의 기초를 들여다 보기 바란다.
  이제 Post class가 잘 동작하는지를 보기 위해 새로운 테스트 케이스를 추가하자. 그전에 먼저 해야할 것이 있다. 현재 구현상 테스트를 수행할 때마다 데이터 베이스에 새로운 데이터가 저장된다. 이와 같은 경우 테스트를 수행하면 할 수록 불필요한 데이터가 계속 쌓이게 될것이다.
  이와 같은 문제를 방지하기 위해 BasicTests.java에 다음과 같이 setup() 메소드를 추가하자.
public class BasicTest extends UnitTest {
 
    @Before
    public void setup() {
        Fixtures.deleteDatabase();
    }
    …
}

@Before 어노테이션은 JPA의 핵심 어노테이션 중 하나이다. @Before 어노테이션에 의해 테스트 메소드가 수행되기 전에 setup() 메소드가 최초로 한번 수행되게 된다.
Fixture 클래스는 database를 쉽게 조작하기 위해 만들어진 play의 utility 클래스로 대표적으로 아래 두개의 기능을 갖는다:
- deleteAll(): testing을 위해 database를 초기화 시킨다.
- load(filename): YAML 포맷의 초기 데이터를 읽어와 database에 저장한다. 

  자 이제 아래 처럼 새로운 테스트 케이스를 추가한다.
@Test
public void createPost() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "secret", "Bob").save();
    
    // Create a new post
    new Post(bob, "My first post", "Hello world").save();
    
    // Test that the post has been created
    assertEquals(1, Post.count());
    
    // Retrieve all posts created by Bob
    List<Post> bobPosts = Post.find("byAuthor", bob).fetch();
    
    // Tests
    assertEquals(1, bobPosts.size());
    Post firstPost = bobPosts.get(0);
    assertNotNull(firstPost);
    assertEquals(bob, firstPost.author);
    assertEquals("My first post", firstPost.title);
    assertEquals("Hello world", firstPost.content);
    assertNotNull(firstPost.postedAt);
}
Adding Comments

자 이제 마지막으로 추가해야 할것은 바로 Comment 이다. 쉽게 말하면 댓글기능을 추가해보자. Comment 모델 클래스는 매우 직관적이다.
package models;
 
import java.util.*;
import javax.persistence.*;
import play.db.jpa.*;
 
@Entity
public class Comment extends Model {
 
    public String author;
    public Date postedAt;
     
    @Lob
    public String content;
    
    @ManyToOne
    public Post post;
    
    public Comment(Post post, String author, String content) {
        this.post = post;
        this.author = author;
        this.content = content;
        this.postedAt = new Date();
    }
 
}

Post 와 마찮가지로 @Lob 어노테이션으로 Large text를 저장하고 있고, 하나의 Post 에 여러개의 댓글을 달수 있다는 의미로 @ManyToOne 어노테이션을 사용했다. Comment 모델 글래스를 완성했으니 다시 Test case를 작성해보자 /yabe/test/BasicTests.java 파일에 postComments() 란 테스트 메소드를 추가한다.
@Test
public void postComments() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "secret", "Bob").save();
 
    // Create a new post
    Post bobPost = new Post(bob, "My first post", "Hello world").save();
 
    // Post a first comment
    new Comment(bobPost, "Jeff", "Nice post").save();
    new Comment(bobPost, "Tom", "I knew that !").save();
 
    // Retrieve all comments
    List<Comment> bobPostComments = Comment.find("byPost", bobPost).fetch();
 
    // Tests
    assertEquals(2, bobPostComments.size());
 
    Comment firstComment = bobPostComments.get(0);
    assertNotNull(firstComment);
    assertEquals("Jeff", firstComment.author);
    assertEquals("Nice post", firstComment.content);
    assertNotNull(firstComment.postedAt);
 
    Comment secondComment = bobPostComments.get(1);
    assertNotNull(secondComment);
    assertEquals("Tom", secondComment.author);
    assertEquals("I knew that !", secondComment.content);
    assertNotNull(secondComment.postedAt);
}
테스트 코드를 보면 특정 Post 에 해당하는 여러개의 Comment를 저장하기 위한 방법이 복잡하다는 걸 느낄 것이다. 특히 각각의 Comment를 저장하기 위해 Comment마다 save(쿼리)를 호출 해주고 있다. 조금 더 좋은 접근을 위해 Post 클래스를 수정해보자. Post 클래스에 comment 필드 삽입:
...
@OneToMany(mappedBy="post", cascade=CascadeType.ALL)
public List<Comment> comments;
 
public Post(User author, String title, String content) { 
    this.comments = new ArrayList<Comment>();
    this.author = author;
    this.title = title;
    this.content = content;
    this.postedAt = new Date();
}
...
mappedBy="post" 는 Comment 클래스의 post field 가 이 두 객체(테이블)의 관계를 소유(owning) 하고 있다는 것을 의미한다. 쉽게 말해 두 객체가 관계를 맺고 그 중 한개가 '주'가 되는것이다. JPA에서 양방향의 어떤 관계를 정의할때는 이와 같이 관계의 주가 되는 객체를 정해주는 것이 중요하다. 지금과 같은 경우는 비록 여러 Comment 가 하나의 Post에 속해있지만 '주'가 되는것은 Comment 쪽의 post field로 정해줬다. (inverse relation 이라고 부르는 것 같다) cascade=CascadeType.ALL 을 정해주므로써 Post 하나를 지우면 그 포스트에 대한 모든 Comment 들을 자동으로 지워주게 된다.

자 이제 모델 객체간의 관계(relationship) 정의는 모두 끝났으니 Post 클래스에 Comment를 쉽게 추가 할 수 있는 헬퍼(helper) 메소드를 추가하자.
public Post addComment(String author, String content) {
    Comment newComment = new Comment(this, author, content).save();
    this.comments.add(newComment);
    this.save();
    return this;
}
이제 잘 동작하는지 확인하기 위한 새로운 테스트 코드를 넣자
@Test
public void useTheCommentsRelation() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "secret", "Bob").save();
 
    // Create a new post
    Post bobPost = new Post(bob, "My first post", "Hello world").save();
 
    // Post a first comment
    bobPost.addComment("Jeff", "Nice post");
    bobPost.addComment("Tom", "I knew that !");
 
    // Count things
    assertEquals(1, User.count());
    assertEquals(1, Post.count());
    assertEquals(2, Comment.count());
 
    // Retrieve Bob's post
    bobPost = Post.find("byAuthor", bob).first();
    assertNotNull(bobPost);
 
    // Navigate to comments
    assertEquals(2, bobPost.comments.size());
    assertEquals("Jeff", bobPost.comments.get(0).author);
    
    // Delete the post
    bobPost.delete();
    
    // Check that all comments have been deleted
    assertEquals(1, User.count());
    assertEquals(0, Post.count());
    assertEquals(0, Comment.count());
}
모두 초록색으로 나오는 것을 볼 수 있다.


Use the Fixture to write more complicated test 좀더 복잡한 테스트를 위해 사전에 준비된 데이터를 이용할 수 있다. Fixture를 이용하면 테스트를 수행하기전에 필요한 데이터(yaml file)를 미리 로딩해서 테스트에 사용할 수 있다. Fixtures.loadModels() 메소드는 yaml 파일으로 부터 데이터를 로드해 database에 로딩해주는 역할을 한다. data.yml 파일의 크기가 좀 크기 때문에 이곳에서 다운받아서 쓰기바란다. data.yml은
/yabe/test/data.yml 에 저장하면 된다. 자 이제 우리는 준비된 데이터를 가지고 assertion 테스트를 수행 할 수 있다. /yabe/test/BasicTests.java 파일에 fullTest() 함수를 아래와 같이 추가하자.
@Test
public void fullTest() {
    Fixtures.loadModels("data.yml");
 
    // Count things
    assertEquals(2, User.count());
    assertEquals(3, Post.count());
    assertEquals(3, Comment.count());
 
    // Try to connect as users
    assertNotNull(User.connect("bob@gmail.com", "secret"));
    assertNotNull(User.connect("jeff@gmail.com", "secret"));
    assertNull(User.connect("jeff@gmail.com", "badpassword"));
    assertNull(User.connect("tom@gmail.com", "secret"));
 
    // Find all of Bob's posts
    List<Post> bobPosts = Post.find("author.email", "bob@gmail.com").fetch();
    assertEquals(2, bobPosts.size());
 
    // Find all comments related to Bob's posts
    List<Comment> bobComments = Comment.find("post.author.email", "bob@gmail.com").fetch();
    assertEquals(3, bobComments.size());
 
    // Find the most recent post
    Post frontPost = Post.find("order by postedAt desc").first();
    assertNotNull(frontPost);
    assertEquals("About the model layer", frontPost.title);
 
    // Check that this post has two comments
    assertEquals(2, frontPost.comments.size());
 
    // Post a new comment
    frontPost.addComment("Jim", "Hello guys");
    assertEquals(3, frontPost.comments.size());
    assertEquals(4, Comment.count());
}
YAML에 대해 궁금하다면 여기를 참고로 하자. YAML 자체는 정말 간단하고 YAML이 Play에만 사용되는 것이 아니므로 한번 훑어 보기를 추천한다.

Play framework으로 블로그 개발하기 (2) - A first iteration of the data model (끝)
저작자 표시
신고
Posted by AppleSherbet

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

티스토리 툴바