[thymeleaf] 4. (select) 객체 안의 객체, 객체 안의 리스트 매핑하기

 


목차

  1. 디렉토리 구조, 결과 화면
  2. Album, Artist, Song 객체 생성
  3. Inner Object, Object List 매핑하기
  4. 전체 코드


[앨범 리스트 조회 프로젝트]

프로젝트를 진행하다보면 단순한 형태의 Object만 사용하게 되는 일은 거의 없다. 필연적으로 Object 안의 Object, Object 안의 List, Object 안의 Object List를 사용해야 하는데 구글링을 해도 한글로 정리된 자료가 많이 나오지 않아 사수님과 해결법을 찾기 위해 고군분투 했다.

나처럼 타임리프를 사용하는 누군가에게 도움이 되기를 바라며 예제를 통해 사용법들을 정리해보았다.


이번 예제는 앨범 정보Album(타이틀, 아티스트명, 발매일, 아티스트 정보, 수록곡 리스트)를 조회하는 간단한 실습을 진행해 볼 예정이다. 간단하게 진행하려고 했는데 조회, 수정을 직접하려다 보니 새로 산 m1 맥북에 개발환경이 아직 구축이 안되어 있어서 DB설치부터 생각보다 시간이 오래걸렸다… :cry:



1. 디렉토리 구조, 결과 화면

디렉토리 구조 접기/펼치기

여기서는 아래 파일을 사용할 예정이다.
Spring(.java) - AlbumController, AlbumService, AlbumRepository, ArtistRepository, SongRepository
Thymeleaf(.html) - album/selectAllAlbumList


결과 화면

image




2. Album, Artist, Song 객체 생성

예제를 위해서 앨범(Album), 아티스트(Artist), 노래(Song) 테이블과 객체를 생성했다. 제대로 하려면 아티스트나 노래 아이디를 이용해서 외래키를 설정해줘야 하지만 지금은 JPA가 아니라 타임리프를 주로 다룰 예제이기 때문에 복잡함을 최소화하고자 하였다. 따라서 따로 연결을 하지 않고 Album 객체에 테이블의 컬럼과 연결되지 않은 artist, songList를 추가해 주었다.

+ 편의를 위해서 Artist.nameUnique를 걸어두었다.


코드는 가독성을 위해 맨 아래에 둘 예정이다. 객체 구조는 아래 그림을 보고 참고하면 될 것 같다 :)

image




3. (Thymeleaf) Inner Object, Object List 매핑

1) AlbumServiceImpl.java
// AlbumServiceImpl.java
public class AlbumServiceImpl implements AlbumService {

    @Autowired
    AlbumRepository albumRepository;

    @Autowired
    SongRepository songRepository;

    @Autowired
    ArtistRepository artistRepository;

    @Override
    public List<Album> selectAlbumList() {

        List<Album> albumList = albumRepository.findAll();

        for(int i = 0; i<albumList.size(); i++) {
            Album album = albumList.get(i);
            String artistName = album.getArtistName();
          
        		// 아티스트 정보 추가
            album.setArtist(artistRepository.findByName(artistName));
            // 앨범 수록곡 리스트 추가
            album.setSongList(songRepository.findByArtistNameAndAlbumTitle(artistName, album.getTitle()));
        }
        
        return albumList;
    }
}



2. AlbumController.java
package com.example.spring.controller;

import java.util.List;

import com.example.spring.model.Album;
import com.example.spring.service.AlbumService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("album")
public class AlbumController {

    @Autowired
    AlbumService albumService;

    @RequestMapping("selectAllAlbumList")
    ModelAndView selectAllAlbumList() {
        // Thymeleaf - selectAllAlbumList.html 파일의 위치
        ModelAndView mav = new ModelAndView("album/selectAllAlbumList");

        List<Album> albumList = albumService.selectAlbumList();
        mav.addObject("albumList", albumList);

        return mav;
    }

}



3) selectAllAlbumList.html
  • songStat의 경우 th:each 를 사용할 경우 기본적으로 제공되는 상태 변수로 index, count 등을 제공한다. songStat가 아닌 다른 이름을 사용하고 싶을 경우 직접 명시해 줄 수 있다.

    자세한 사용법은 2. Thymeleaf 기본 문법 + 사용 예제에서 확인 할 수 있다.

<!DOCTYPE html>
<html>
  <head>
    <title>전체 앨범 리스트</title>
    <style>
      h3 {
        margin-left: 15px;
        margin-bottom: 15px;
      }
      td {
        padding-left: 15px;
        padding-right: 15px;
      }
    </style>
  </head>
  <body>
    <h3>전체 앨범 리스트</h3>
    <table>
      <tr>
        <th>타이틀</th>
        <th>발매일</th>
        <th>아티스트명</th>
        <th>아티스트 정보</th>
        <th>곡 리스트</th>
        <th>수정</th>
      </tr>
      <tr th:each="album: ${albumList}">
        <td th:text="${album.title}"></td>
        <td th:text="${album.released}"></td>
        <td th:text="${album.artistName}"></td>
        <td>
          <!--/* 타임리프에서 문법으로 | 를 사용하여 문자열을 연결 할 수 있다.*/-->
          <p th:text="|이름: ${album.artist.name}|"></p>
          <p th:text="|나이: ${album.artist.age}|"></p>
          <p th:text="|생일: ${album.artist.birth}|"></p>
        </td>
        <td>
          <!-- /* song, state: ${album.songList} 와 같이 stat변수 명을 직접 설정 가능*/ -->
          <p th:each="song: ${album.songList}">
            <!-- /* index가 0부터 시작하기 때문에 +1 */ -->
            <span th:text="${songStat.index}+1"></span>
            <span th:text="|. ${song.title}|"></span>
          </p>
        </td>
      </tr>
    </table>
  </body>
</html>




4. 전체 코드

1) AlbumRepository.java
// AlbumRepository.java
package com.example.spring.repository;

import java.util.List;

import com.example.spring.model.Album;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface AlbumRepository extends JpaRepository<Album, String> {

    public Album findByTitle(String title);

    @Query(value="select a from Album a where a.artistName = :artistName")
    public List<Album> findByArtist_Name(@Param("artistName") String artistName);
    
}


2) ArtistRepository.java
// ArtistRepository.java
package com.example.spring.repository;

import com.example.spring.model.Artist;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ArtistRepository extends JpaRepository<Artist, String> {

    public Artist findByName(String artistName);
    
}


3) SongRepository.java
// SongRepository.java
package com.example.spring.repository;

import java.util.List;

import com.example.spring.model.Song;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface SongRepository extends JpaRepository<Song, String> {
    
    @Query(value="select * from Song s where s.artist_name = :artistName and s.album_title = :albumTitle", nativeQuery = true)
    public List<Song> findByArtistNameAndAlbumTitle(@Param("artistName") String artistName, @Param("albumTitle") String albumTitle);
    
}