회원가입 시 프로필 사진 처리

Jongwon·2023년 2월 11일
2

DMS

목록 보기
9/18

회원가입시에, 프로필사진을 등록할 수 있도록 처리를 하고자 합니다. 현재는 로컬 서버에서 동작하고 있기 때문에 컴퓨터 하드 디스크에 저장하도록 하겠습니다.

🔥이미지 파일을 DB(MySQL)에 저장하지 않습니다. DB에 저장하면 용량을 많이 차지하기 때문에 로컬 스토리지 저장경로만 DB에 저장하고, 필요 시 하드디스크에서 호출하도록 만들어야 합니다.



Domain 및 Repository 생성

앞선 글에서 Domain과 Repository는 미리 생성하였으므로 이번엔 생략하겠습니다. 다음 글을 참고해주세요.


Service 생성 및 수정

파일 관련 처리를 할 서비스를 별도로 생성하고, 이곳에서 ImageDTO를 생성하여 반환할 수 있도록 구성하겠습니다.

FileService

@Service
@Log4j2
public class FileService {

    @Transactional
    public ImageDTO createImageDTO(String originalSourceName, Path path) throws IOException {
        String fileName = originalSourceName.substring(originalSourceName.lastIndexOf("\\") + 1);
        String uuid = UUID.randomUUID().toString();
        String fileUrl = getDirectory(path) + File.separator + uuid + "_" + fileName;

        return ImageDTO.builder()
                .fileName(fileName)
                .uuid(uuid)
                .fileUrl(fileUrl)
                .build();
    }

    private String getDirectory(Path path) throws IOException {
        if(!Files.exists(path)) {
            Files.createDirectory(path);
        }
        return path.toString();
    }
}
  • createImageDTO의 파라미터로 originalSourceName을 받아오는데, 이는 MultipartFile의 경로입니다. 원본 경로~파일명까지 저장되어 있는 문자열 값입니다. path는 이미지를 저장할 로컬 저장소 경로입니다.

  • 로컬 저장소에 저장될 이미지의 파일명은 "UUID_파일명.확장자"형태로 저장됩니다.

  • 해당 경로에 폴더가 없으면 폴더를 생성해주는 getDirectory메서드는 서비스 안에서만 동작하도록 private 설정을 해줍니다.



다음으로 회원가입 기능을 하는 서비스 부분을 변경합니다.

MemberService

@Service
@Log4j2
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {

    private final MemberRepository memberRepository;
    private final TokenService tokenService;
    private final AuthService authService;
//추가
    private final FileService fileService;
    private final MemberImageRepository imageRepository;
    
//추가
    @Value("${spring.servlet.multipart.location}")
    private String uploadPath;

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
        return memberRepository.findByUserId(userId)
                .map(this::createUserDetails)
                .orElseThrow(() -> new UsernameNotFoundException("userId: " + userId + "를 데이터베이스에서 찾을 수 없습니다."));
    }

    private UserDetails createUserDetails(Member member) {
        GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(member.getRoles().stream().map(Role::getType).collect(Collectors.joining(",")));

        return new User(
                member.getUserId(),
                member.getPassword(),
                Collections.singleton(grantedAuthority)
        );
    }

    public Member findMemberByUserId(String userId) {
        return memberRepository.findByUserId(userId).orElseThrow(() -> new RuntimeException("해당 ID를 가진 사용자가 존재하지 않습니다."));
    }

    public MemberDTO findMemberByEmail(String email) {
        Member member = memberRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("해당 email을 가진 사용자가 존재하지 않습니다."));
        return MemberMapper.INSTANCE.memberToMemberDTO(member);
    }

    public MemberDTO getMember(String userId) {
        return MemberMapper.INSTANCE.memberToMemberDTO(findMemberByUserId(userId));
    }

    @Transactional
    public void saveMember(MemberDTO memberDTO) {
        memberRepository.save(MemberMapper.INSTANCE.memberDTOToMember(memberDTO));
    }

    /**
     * UsernamePasswordAuthenticationToken을 통한 Spring Security인증 진행
     * 이후 tokenService에 userId값을 전달하여 토큰 생성
     * @param requestDTO
     * @return TokenDTO
     */
    @Transactional
    public TokenDTO login(LoginRequestDTO requestDTO) {
        authService.authenticateLogin(requestDTO);

        Member member = memberRepository.findByUserId(requestDTO.getUserId()).get();
        MemberDTO memberDTO = MemberMapper.INSTANCE.memberToMemberDTO(member);
        return tokenService.createToken(memberDTO);
    }

    @Transactional
    public void signup(MemberRequestDTO requestDTO) {
        if(memberRepository.existsByUserId(requestDTO.getUserId())) {
            throw new RuntimeException("이미 존재하는 아이디입니다.");
        }

        Member member = MemberMapper.INSTANCE.memberRequestDTOToMember(requestDTO);
        member.updateRole(Role.ROLE_USER);
        memberRepository.save(member);

//추가
        if(!(requestDTO.getMemberImage() == null)) {
            MemberImage memberImage = saveMemberImage(requestDTO.getMemberImage());
            member.updateMemberImage(memberImage);
        }

    }

//추가
    @Transactional(readOnly = false)
    private MemberImage saveMemberImage(MultipartFile file) {
        if(file.getContentType().startsWith("image") == false) {
            log.warn("이미지 파일이 아닙니다.");
            return null;
        }

        String originalName = file.getOriginalFilename();
        Path root = Paths.get(uploadPath, "member");

        try {
            ImageDTO imageDTO =  fileService.createImageDTO(originalName, root);
            MemberImage memberImage = MemberImage.builder()
                    .uuid(imageDTO.getUuid())
                    .fileName(imageDTO.getFileName())
                    .fileUrl(imageDTO.getFileUrl())
                    .build();

            file.transferTo(Paths.get(imageDTO.getFileUrl()));

            return imageRepository.save(memberImage);
        } catch (IOException e) {
            log.warn("업로드 폴더 생성 실패: " + e.getMessage());
        }

        return null;
    }
}
  • file.transferTo에서 파일이 로컬 스토리지에 저장됩니다.
  • Member로 매핑한 이후 생성한 MemberImage를 넣어주고, 다시 DB에 저장합니다.

🔥 spring.servlet.multipart.location은 yml파일에 추가한 값입니다. 이미지 파일이 저장될 로컬 경로를 나타낸 것으로, 이외에도 파일 최대 크기 설정도 진행합니다.
application.yml

spring:
    servlet:
    	multipart:
      		max-file-size: 4MB
      		max-request-size: 4MB
      		location: 지정할 경로




참고자료
https://hello-bryan.tistory.com/343
https://chb2005.tistory.com/102
https://ssdragon.tistory.com/99

profile
Backend Engineer

0개의 댓글