Firo

파일을 첨부, 다운로드 하는데 Firo를 사용하는 이유는 다음과 같습니다.

  • 파일 첨부하는 로직을 메인 비즈니스 로직으로 부터 분리.
  • 추가 SQL 문 실행 없이 파일 경로를 얻기

1. 개요

Firo 를 사용하는 도메인은 다른 도메인과 구분되는 자신의 유일한 domain code 를 가져야 합니다.

도메인(domain code) 는 일반적으로 테이블과 매칭 되며, domain + domain pk 가 도메인별 유일한 값이 되어 Firo 시스템에 보관되게 됩니다.

TIP

도메인은 도메인(domain + domain pk) 내에 여러개의 category 를 가질 수 있습니다.
category 은 비즈니스 요구사항에 맞춰 동적으로 만들 수 있으며, 파일 첨부시 category 을 지정하지 않으면 default 라는 코드를 가지고 있는 category 로 들어가게 됩니다.

여기에서 설명하는 애플리케이션은 다음 두개의 도메인에서 첨부 파일을 사용한다고 가정합니다.

domain codecategory codedesc
boarddefault게시판의 첨부파일입니다. 1개의 기본 category 만 사용합니다.
productprimary상품의 대표 이미지를 저장하는 category 입니다.
desc상품을 설명하는 이미지를 저장하는 category 입니다. 여러장의 이미지를 첨부 할 수 있습니다.

2. 설치 및 설정

2.1. 의존성 추가

maven pom.xml 파일에 다음과 같이 firo 의존성을 추가합니다.

<dependency>
  <groupId>com.unvus.firo</groupId>
  <artifactId>firo</artifactId>
  <artifactId>0.0.3</artifactId>
</dependency>

기본적인 설정은 다음과 같습니다.

spring yml 을 통한 설정

항목기본값desc
directory.tmpDirSystem.getProperty(“java.io.tmpdir”)임시파일 저장 위치입니다.
directory.baseDirSystem.getProperty(“java.io.tmpdir”)파일을 저장할 위치의 상위 디렉토리 입니다.
디렉토리 정책에 따라 이 아래로 하위 디렉토리가 생기게 됩니다.
database.**h2 메모리 database파일 매핑 정보를 보관할 테이블의 데이터베이스 정보입니다.

java config

항목기본값desc
DirectoryPathPolicyDateDirectoryPathPolicy위 baseDir 아래로 디렉토리를 구성합니다. 위 tmpDir, baseDir 에 의존적입니다.
AdapterAdapterType.LOCAL파일 저장 adapter.
서버의 디스크, FTP 서버, S3 등 파일을 저장하는 로직을 정의합니다.

의존성 추가 후 아무런 설정을 하지 않아도, 위의 기본값으로 구동이 됩니다.
하지만 이러한 기본 구성은 테스트 용도로만 사용가능하며, 실제 업무에서는 파일과 데이터를 영구적으로 저장하지 못하므로 사용할 수 없습니다.

2.2. 기본 설정

다음과 같이 Spring yml 로 firo 를 구성 할 수 있습니다.

unvus:
  firo:
    directory:
      tmp-dir: /data/unvus/attach/dev/tmp
      base-dir: /data/unvus/attach/dev
    database:
      driver-class-name: com.zaxxer.hikari.HikariDataSource
      url: jdbc:mysql://unvus.com:3360/myapp
      username: myapp-user
      password: myapp-password

나머지 디렉토리 정책은 DateDirectoryPathPolicy 으로, 어댑터는 AdapterType.LOCAL 으로 자동 설정 되며,
이 값을 변경하기 원하면 spring java config 로 유연하게 설정 하도록 합니다.

TIP

서버의 파일 저장 디렉토리가 NFS(Network File System) 으로 묶여 있다면, 위 구성만으로 운영이 가능합니다.

디렉토리 정책

DirectoryPathPolicy 는 현재 DateDirectoryPathPolicy 만 제공되고 있습니다.

더 상세한 설정은 Case 별 상세 설정 에서 확인하도록 합니다.

2. Service Side Usage

브라우저로 부터 온 첨부파일을 서버(java code)단에서 처리하기 위한 방법을 설명합니다.

2.1. annotation 기반

2.1.1. @FiroDomain

domain 클래스에 @FiroDomain 어노테이션을 추가합니다.

@FiroDomain("board")
@Data
public class BoardPost {

    private Long id;

    private String title;

    private String content;
}

@FiroDomain 어노테이션에 인자로 넘긴 “board” 는 domainCode(도메인 코드) 입니다.

2.1.2. @FiroUpload

저장(수정) RestAPI resource 에 @FiroUpload 어노테이션을 추가합니다.

@FiroUpload
@PostMapping("/board")
public ResponseEntity<Void> addBoard(@RequestBody Board board) {
    boardService.saveBoard(board);
    return ResponseEntity.ok().build();
}

@FiroUpload 상세

addBoard 가 오류 없이 실행되고 나면, @FiroUpload 는 파일을 저장하고,
BoardPost 의 도메인 코드인 board 와 primary key 인 id 값을 가지고 도메인과의 맵핑정보를 저장하게 됩니다.

이렇게 저장된 이미지는 다음과 같은 패턴의 주소로 불러 올 수 있습니다.

/assets/firo/attach/view/{domain-code}/{domain-pk}/{category-code}

예를 들어 저장된 Board 의 id 가 35 라면, 다음과 같이 이미지를 불러올 수 있습니다.

<img src="/assets/firo/attach/view/board/35/default"/>

2.2. FiroService 직접 사용

Annotation 으로 편리하게 사용 할 수 있지만, 때로는 개발자가 직접 파일 업로드를 컨트롤 하고 싶을 수 있습니다.

2.2.1. FiroService 가져오기

Firo 의 FiroService 는 Spring Bean 으로 등록되어 있으므로 다음과 같은 의존성 삽입으로 인스턴스를 얻을 수 있습니다.

@Service
public class ProductService {

    private final ProductRepository productRepository;
    
    private final FiroService firoService;

    public ProductService(ProductRepository productRepository, FiroService firoService) {
        this.productRepository = productRepository;
        this.firoService = firoService;
    }
}

2.2.2. FiroService 사용하기

그리고 다음과 같이 수동으로 첨부 파일을 등록 할 수 있습니다.

    @Transactional
    public Product saveProduct(Product product) throws Exception {
        if (product.getId() == null) {
            addProduct(product);
        } else {
            modifyProduct(product);
        }
        
        // request body 로 부터 첨부파일 정보 획득 
        AttachContainer attachContainer = FiroWebUtil.getAttachContainer();
        
        // 첨부파일 저장
        firoService.save(product.getId(), attachContainer.get("product"), product.getCreatedDt());
        return product;
    }

# request body

요청(HttpServletRequest) body 에 전달되어온 첨부파일 정보는 다음과 같이 얻을 수 있습니다.

AttachContainer attachContainer = FiroWebUtil.getAttachContainer();

한번의 요청에 여러개의 domain 이 포함 되어 있을 수 있습니다.

example

상품 등록 화면에는 Product 도메인 관련 이미지와 Option 도메인 관련 이미지 가 함께 있을 수 있습니다.

# AttachBag

AttachContainer 는 여러개의 domain 을 포함하고 있으며, 다음과 같이 특정 domain 을 꺼낼 수 있습니다.

AttachBag prodBag = attachContainer.get("product");
AttachBag optBag = attachContainer.get("product_option");

하나의 AttachBag 은 하나의 domain 을 담당하며, AttachBag 에는 domain 내의 각 category 별로 구분하여 첨부파일(및 부가 정보)을 포함 하고 있습니다.

# 필수사항

Firo 가 파일 정보를 저장하기 위해서는, 해당 도메인의 primary key생성 일자를 필요로 합니다.

  • primary key 는 첨부파일과 도메인을 연결하기 위해 필요하며,
  • 생성 일자 는 저장되는 물리적 디렉토리를 지정하는데 필요합니다.
    ("수정 일자"는 매번 변경되기 때문에 반드시 "생성 일자"를 사용해야 합니다.)

WARNING

많은 회사에서 하나의 디렉토리에 모든 첨부 파일을 보관하곤 합니다. 이렇게 보관하면 해당 디렉토리를 리스팅 하는데 심각한 어려움을 겪게 됩니다.
최대한 그룹화(도메인, 날짜 등으로) 해서 sub directory 를 만들어서 물리적 파일을 보관할 것을 권합니다.

이제 다음과 같이 필수사항을 포함해서 저장을 합니다.

firoService.save(product.getId(), attachContainer.get("product"), product.getCreatedDt());

3. Client Side Usage

브라우져단에서 firo 파일(이미지)를 처리하는 방법을 설명합니다.

3.1. 파일(이미지) 가져오기

파일(이미지)는 크게 2가지 방법으로 가지고 올 수 있습니다.

firo 에서 제공하는 file controller
  • 실시간 thumbnail 기능이 제공됩니다.
  • 실시간 watermark 기능이 제공됩니다.
  • 파일에 접근 권한을 제어 할 수 있습니다.
  • browser cache header 를 통해 이미지 캐시를 제공합니다.
  • 데이터베이스 접근이 필요합니다.
파일 저장소로 부터 direct access
  • 데이터베이스 접근이 필요 없습니다.
  • 서버 구성에 따라 image controller 보다 빠른 엑세스가 가능힙니다.
  • image controller 이 제공하는 다양한 기능을 이용 할 수 없습니다.

3.1.2. file controller

Firo 에서 제공하는 file controller 를 통해, 파일(이미지)를 가져오는 방법을 설명합니다.
이 방식은 첨부파일 접근시 database access 를 필요로하지만, db cachebrowser cache 를 통해 최대한 성능에 이상이 없도록 구현되어있습니다.

# 기본

이미지는 다음과 같이 가져 올수 있습니다.

/assets/firo/attach/view/{domain-code}/{domain-pk}/{category-code}

# 배열

만약 해당 cabinet 이 여러 파일을 포함하고 있다면 인덱스를 추가하여 접근 할 수 있습니다.

/assets/firo/attach/view/{domain-code}/{domain-pk}/{category-code}/{index}

# thumbnail

Firo 는 실시간 thumbnail 생성이 가능합니다.
(한번 생성한 thumbnail 은 실제 이미지 파일로 저장해서, 다음 요청시에는 이미지 생성단계를 스킵합니다.)

/assets/firo/attach/view/{domain-code}/{domain-pk}/{category-code}?w=200&h=200

parameterdesc
wthumbnail 의 넓이 px. 숫자만 허용
hthumbnail 의 높이 px. 숫자만 허용

두 값(w, h)중 하나만 제공할 경우에는 원본 이미지의 비율에 맞춰 나머지 값은 자동으로 설정됩니다.

3.1.3. direct access

파일 저장 방식이 LocalAdapter 가 아닌 FtpAdapterS3Adapter 를 사용하는 경우라면,
파일(이미지)를 file controller 를 거치지 않고 해당 서버에서 직접 가져오는 것이 성능에 유리합니다.

이 기능을 사용하려면, 파일이 저장된 서버에 web 으로 접근할 수 있는 url 정보가 필요합니다.

이는 다음과 같이 spring yml 설정으로 제공해줄 수 있습니다.

unvus:
  firo:
    direct-url: https://front.mydomain.com/attach/dev/

WARNING

S3Adapter 경우 cloud front 주소나 S3 주소를 제공해주면 되나,
FtpAdapter 경우에는 해당 FTP 서버에 nginx 나 apache 같은 웹서버가 파일 저장 디렉토리를 서비스 할 수 있도록 구성되어져 있어야 합니다.

firo 가 파일을 저장하는 디렉토리 구조는 다음과 같습니다.

{base-dir}{domain-code}/{yy}/{MM}/{domain-pk}/{category-code}/{secure-name}

itemdesc
base-diryml 에 설정한 base-dir 입니다.
domain-code도메인에 할당한 domain code 입니다.
yy도메인 모델의 생성일시의 년도 2자리 입니다.
MM도메인 모델의 생성일시의 월 2자리 입니다.
domain-pk도메인 모델의 primary key 값입니다.
category-codedomain 내의 category 코드입니다.
secure-name저장된 파일명입니다.

direct access 방식으로 url 을 사용할 경우, 사용자가 패턴을 추적하여 다른 파일을 획득 하지 못하도록 하기 위해서,
최종 파일명은 secure-name 으로 저장됩니다.

secure-name

  • 사용자가 파일명을 예측하지 못하게 합니다.
  • SecureNameUtil.gen(category, domain-pk, index) 을 통해서 얻을 수 있습니다.
    • 같은 domain, domain-pk, category, index(정렬 순번) 이면 항상 같은 값을 반환합니다.
  • yml 의 unvus.firo.secret 키값을 기반으로 생성됩니다. 즉, 관리자, 사용자단 모두 같은 unvus.firo.secret 값을 사용해야 합니다.

Firo 에서는 direct access 를 통해 이미지를 보여줄 수 있는, custom taglib 를 제공합니다.

다음은 해당 태그를 사용하는 간단한 예입니다.

<img src="<tags:firo-url model="${product}" domain="product" category="thumb_m"/>"  style="width: 300px;"/>
item필수기본값desc
model필수domain model java 객체입니다.
domain필수domain code 입니다.
categorydefaultcategory code 입니다.
domainIdmodel.idmodel 의 domain-pk 값입니다.
index0여러 파일일 경우 인덱스 정보입니다
createdDtmodel.createdDtdomain 이 생성된 일시 정보입니다.
modifiedDtmodel.modifiedDtdomain 의 최근 수정된 일시 정보입니다.
cacheValuemodifiedDt:milli secondsbrowser 캐시를 위한 값입니다.
useDirectUrltruedirect access 를 사용할지 여부입니다.

3.2. Vue.js

Firo 에서는 vue 환경에서 사용 할 수 있는 firo-file-upload 컴포넌트를 제공합니다.

기본 사용법은 다음과 같습니다.

<firo-file-upload
  :model="product"
  :ref-target-key="product.id"
  ref-target="product"
  ref-target-type="primary"
></firo-file-upload>

다음은 firo-file-upload 를 사용 하는 간단한 예를 보여줍니다.

<template>
  <form @submit="save">
    <div class="mb-3">
      <label class="form-label">상품명</label>
      <input type="text" v-model="product.name">
    </div>
    <div class="mb-3">
      <label class="form-label">상품 이미지</label>
      
      <!-- firo file upload -->
      <firo-file-upload
        :model="product"
        :ref-target-key="product.id"
        ref-target="product"
        ref-target-type="primary"
      ></firo-file-upload>
      <!--// firo file upload -->
      
    </div>
    <button type="submit">Submit</button>
  </form>
</template>

<script>
  import FiroFileUpload from 'firo-file-upload';

  export default {
    components: {FiroFileUpload},
    data() {
      return {
        product: {},
      };
    },
    methods: {
      save() {
        this.$axios.post('/product', this.product);
      }
    }
  }
</script>

이 컴포넌트는

  • 전달받은 model 객체에, attachContainer 라는 파일정보를 담을 속성을 자동으로 추가해서 구성합니다.
  • 사용자가 파일을 선택하면, 즉시 서버에 임시 저장하고 해당 토큰값을 domain, category 정보와 함께 attachContainer 에 보관합니다.
  • 이미지의 경우 thumbnail 이미지를 보여줍니다.
  • 선택된 파일을 삭제 할 수 있도록 삭제 버튼을 생성합니다.

이미 저장된 도메인을 수정하는 화면에도 같은 방식으로 사용 하며, ref-target-key 값이 존재하는 경우에는

  • 서버로 부터 첨부파일 정보를 가져옵니다.
  • 해당 파일들을 화면에 보여줍니다.