$docker pull python:3.9-buster
# 컨테이너 생성 후 /bin/bash 실행
$ docker container run -it --name=flask_server -p 80:5000 python:3.9-buster /bin/bash
$ git clone <flask server git url>
$ apt-get update
$ apt-install vim
# config.py 생성
$ vim config.py
# checkpoints copy
$ docker cp <host path> <container path>
$ pip install -r requirements.txt
$ pip install torch==1.10.0+cu113 torchvision==0.11.1+cu113 torchaudio==0.10.0+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html
$ pythoon app.py
WebSocket HandshakeInterceptor 이용해서 클라이언트 식별
public class WebSocketAuthenticationInterceptor implements HandshakeInterceptor {
private final MemberRepository memberRepository;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
JwtUserDetails principal = (JwtUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Long memberId = principal.getId();
Member member = memberRepository.findById(memberId).orElseThrow(() -> new EntityNotFoundException("찾을 수 없는 사용자입니다."));
attributes.put("name", member.getName());
attributes.put("imageUrl", member.getProfileImageUrl());
return true;
}
웹소켓에 보낼 프로필 이미지 인코딩 할 때 에러 발생시 기본 이미지로 인코딩 시도. 기본 이미지도 인코딩 실패 할 때는 빈 문자열로 지정.
public void convertImageToBase64(String imageUrl) {
URL url = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
url = new URL(imageUrl);
is = url.openStream();
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
byte[] imageBytes = baos.toByteArray();
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
is.close();
baos.close();
this.encodedImage = base64Image;
}
catch (Exception e) {
log.error("Exception convertImageToBase64");
if(imageUrl == "https://vingterview.s3.ap-northeast-2.amazonaws.com/image/ced77a75-31f1-47ce-82a0-6923b55cb7bb.png"){
log.error("기본 이미지 인코딩 실패");
this.encodedImage = "";
return;
}
convertImageToBase64("https://vingterview.s3.ap-northeast-2.amazonaws.com/image/ced77a75-31f1-47ce-82a0-6923b55cb7bb.png");
}
}
게시글 혹은 댓글 수정 시 자신이 작성한 글이 아닐 경우 403 FORBIDDEN 응답 하도록 코드 수정. 과정에서 AccessDeniedAcception을 던지는데 에러를 각 컨트롤러 메서드에서 하나씩 받아서 처리할려고 하니 SRP , OCP를 위반하는 것 같아 ExceptionHandler 이용해서 처리하도록 함.
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(AccessDeniedException.class)
public ErrorResult accessDeniedExHandle(AccessDeniedException e) {
log.error("[accessDeniedExHandle] ex", e);
return new ErrorResult("403", e.getMessage());
}
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(EntityNotFoundException.class)
public ErrorResult entityNotFoundExHandle(EntityNotFoundException e) {
log.error("[entityNotFoundExHandle] ex", e);
return new ErrorResult("404", e.getMessage());
}
딥페이크 기술을 이용한 영상과 오디오를 합쳐서 최종 결과물 s3에 업로드 하는 파이프라인 구축
-> 조금 더 빠르게 처리할 수 있는 방법은?
위에가 원본, 아래가 딥 페이크 기술 적용한 영상
비디오 스케일링 : ffmpeg -i {input_video_path} -vf scale=360:360 {output_audio_path}
오디오 추출 : ffmpeg -y -i {input_video_path} {output_audio_path}
비디오 오디오 병합 : ffmpeg -y -i {input_video_path} -i {input_audio_path} -c:v copy
-map 0:v:0 -map 1:a:0 -shortest {output_video_path}
-c:v copy: 비디오 스트림을 원본과 동일하게 복사
-map 0:v:0: 첫 번째 입력 파일의 첫 번째 비디오 스트림을 선택
-map 1:a:0: 두 번째 입력 파일의 첫 번째 오디오 스트림을 선택
WebSocket 서버 개발 완료
WebSocket 통신 프로토콜 정의 및 통신 과정
해야할 것: 1. WebSocket 이용해서 룸 만들기 2. 매칭 알고리즘 3. Flutter client코드 작성
AWS S3 버킷 생성해서 버킷에 프로필 업로드 할 수 있도록 변경
ACCESSKEY,SECRETKEY 관리 어떻게 할까?
https://javabom.tistory.com/95
-> EnvironmentVariableCredentialsProvider 자바 환경 변수로 등록
-> 배포할 때는 InstanceProfileCredentialsProvider 방식 이용
버킷 권한 설정? buckey policy and ACLs
https://stackoverflow.com/questions/47815526/s3-bucket-policy-vs-access-control-list
https://aws.amazon.com/ko/blogs/storage/understanding-s3-block-public-access/
스프링 부트 3.x 버전 이상 부터는 springdoc을 사용해야함
https://resilient-923.tistory.com/414
그리고 swagger 적용하니 다음과 같은 예외 발생 Error calling jakarta.validation.Validation#buildDefaultValidatorFactory
sprig-boot-starter-validation 추가하면됨.
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
그리고 Security를 적용하고 있어서 404에러는 발생하지 않으나 swagger url로 접속시 아무런 page도 나오지 않음. -> swagger 관련 url permit해야함.
("/v3/api-docs/\**", "/swagger-ui/\**").permitAll()
v3부분은 각 버전에 알맞게 변경
https://jong-bae.tistory.com/12
시험기간을 지나서 다시 개발 진행
현재 문제가 되는 부분은 응답을 먼저 보내는데 이때 동영상 처리과정에서 오류가 발생했을 때 클라이언트에 이를 알릴 수 있는 방법이 마땅히 없다.. 좀 더 찾아봐야 할 것 같음
또한 변환 성능을 높이기 위해 source video에 사람 얼굴이 정면에 제대로 잘 나와야함 -> client 단에서 video upload할 때 가이드 라인 제시하면 ? 좋을듯
질문: service->controller로 넘겨줄 때 Domain을 넘겨주는 것이 맞을까 DTO로 넘겨주는 것이 맞을까. TEST Case 작성하면서 궁금해짐. 밑에 글을 보면 도메인을 controller 단까지 전달하면 controller가 Model과 결합도가 증가하여 유지 보수 측면에서 좋지 않다고함.
크기가 큰 파일(동영상)들은 어떻게 효율적으로 업로드 할 수 있을지?
-> 클라이언트에서 파일을 분할하여 전송하면 -> 병렬적으로 처리
-> 클라이언트 사용성 측면에서 비동기 개발 고려
-> 동영상 압축, 용량 제한 정책 검토
-> 인스타그램에서 동영상 게시물을 업로드 하는 걸 보면 게시글을 업로드한 후 status bar가 나옴-> 이를 도입하는 것도 괜찮을 것 같음.
로그인 기능
-> 로그인 기능을 본격적으로 개발하기 위해서는 개인 정보 정책부터 인코딩 디코딩 등 관리해야할 게 너무 많음
-> 현업에서는 소셜 로그인 기능을 이용해서 구글이나 카카오 측에 위임함
-> 현재 짧은 시간안에 개발을 해야하는 상황에서 로그인보다는 다른 중심 기능을 개발하는 것이 우선적이라 판단해서 로그인 기능을 직접 개발보다 소셜 로그인 기능 혹은 간단한 사용자 인증만 할 예정
@PostMapping("/video")
public ResponseEntity<String> videoUpload(@ModelAttribute BoardVideoDTO boardVideoDTO) {
if (!boardVideoDTO.getVideo().isEmpty() && boardVideoDTO.getVideo() != null) {
String storeFileName = videoStore.createStoreFileName(boardVideoDTO.getVideo().getOriginalFilename());
log.info("----------uploadFile----------start {} {}", LocalDateTime.now(),Thread.currentThread().getName());
videoStore.uploadFile(boardVideoDTO.getVideo(),storeFileName);
log.info("----------UploadFile----------returned {} {}", LocalDateTime.now(),Thread.currentThread().getName());
return ResponseEntity.ok(storeFileName);
}
return ResponseEntity.badRequest()
.body("잘못된 비디오 업로드 요청입니다.");
}
@Override
@Async("threadPoolTaskExecutor")
public void uploadFile(MultipartFile multipartFile, String storeFileName) {
try {
log.info("Started uploading file at {} {}", LocalDateTime.now(),Thread.currentThread().getName());
multipartFile.transferTo(new File(getFullPath(storeFileName)));
log.info("Ended uploading file at {} {}", LocalDateTime.now(),Thread.currentThread().getName());
} catch (IOException e) {
log.warn("업로드 폴더 생성 실패 {}", e.getMessage());
}
}
public interface MyInterface {
void doSomething();
}
@Component
@Qualifier("beanA")
public class MyBeanA implements MyInterface {
public void doSomething() {
// ...
}
}
@Component
@Qualifier("beanB")
public class MyBeanB implements MyInterface {
public void doSomething() {
// ...
}
}
@Service
public class MyService {
@Autowired
@Qualifier("beanA")
private MyInterface beanA;
@Autowired
@Qualifier("beanB")
private MyInterface beanB;
// ...
}
@Component
@Primary
public class MyBeanA implements MyInterface {
public void doSomething() {
// ...
}
}
@Component
public class MyBeanB implements MyInterface {
public void doSomething() {
// ...
}
}
@Service
public class MyService {
@Autowired
private MyInterface myBean;
// ...
}
두 가지 방법을 계속해서 적용해보았지만 에러가 해결 안됨.
UnsatisfiedDependencyException
에러 메시지를 봤을 때 VideoStore 빈의 의존성 주입이 실패했으므로,
1. VideoStore 빈의 설정이 올바른지 확인해 봐야함 -> 잘 되어 있음.
VideoStore 빈이 의존하는 다른 빈이 존재하는지 -> NOPE
FileStore 인터페이스를 정확하게 구현하고 있는지..? 아 아니네,,, 역시나.... 여기서 문제 였음.
좋아요 최적화 하기
https://tecoble.techcourse.co.kr/post/2022-10-10-like-count/
좋아요 테이블 변경
status 추가 like unlike
-> like, unlike 연산 2번으로 줄일 수 있음.
insert는 4번 delete는2번
insert할땐 boardmemberlike에 있는지 조회하고 member, board 조회하고 insert쿼리 -> 4번
delete할땐 boaedmemberlike 조회하고 삭제쿼리 -> 2번
status 추가하면
최초 boardmemberlike에 추가할 때만 4번 쿼리 나가고
이후에는 조회와 status를 update하는 쿼리만 나간다.
이렇게 해서 like를 계속 눌렀을 때 문제를 어느정도 해소.
-> 애초에 like요청을 1분에 몇번만 날릴 수 있도록 하는 게 맞을 듯
enum LikeType
public enum LikeType {
LIKE,UNLIKE
}
/**
* BoardDTO
*/
private Long id;
private Long questionId;
private String questionContent;
private Long memberId;
private String memberNickname;
private String profileImageUrl;
private String content;
private String videoUrl;
private int likeCount;
private int commentCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
board와 member, question은 ManyToOne 관계 -> fetch join
like,comment는 OneToMany -> batchsize 조절해서 in 으로 batch로 가져 올 수 있도록함.
1.예외처리
2.api 변경: member fatch -> 로그인 아이디는 뺴놓기
3.게시글 업로드/수정, 회원가입/회원정보수정에서 영상이나 이미지 업로드하는거를 따로 api를 파서 업로드하면 저장된 주소를 돌려주고, 게시글,회원 post/put할때 요 경로 담아서 요청하는 방향으로
by impala
4.1+n쿼리 문제
적절한 Dto 설계에 대해 공부
프로필 이미지는 s3에 저장할 예정
-> 일단 지금 당장은 s3를 사용하지 않기 때문에
FileStore 인터페이스 만들어서 나중에 구현할 수 있도록, 지금은 ImgStoreMemory 구현 객체로 개발 진행.
TestCode를 작성하면서 UnexpectedRollbackException 발생
https://techblog.woowahan.com/2606/
https://skyblue300a.tistory.com/15
spring data jpa 정리
+) 기능 설계할 때 MEMBER가 여러 TAG를 적용할 수 있도록 할지에 대한 이야기가 부족했던 것 같음 -> 팀원들과 이야기 해본 결과 여러 TAG 적용할 수 있도록 함.
파일 업로드를 어떻게 처리해야 할까?
아무래도 동영상을 업로드해야하는데 네트워크 사용량이나 비정상적인 요청들을 고려하지 않을 수가 없음
일단 우리 서비스는 동영상 크기가 커봤자 1GB 안 쪽일 것이라고 판단. 1개의 질문에 대한 면접 영상은 길어봤자 2분 정도일 것이고 4K - 60fps 라도 800MB일 것임.
질문
많은 사용자가 갑자기 많이 요청을 보내면?
-> 직접 여러개 브라우저 켜서 보내봤다..
-> 괜찮은듯, 스레드 풀안에서 하니까?
멀티파트로 사이즈가 큰 데이터 업로드 시 메모리 오류가 안나는 이유는? InputStream 이 memory에 모든 데이터를 올려 놓은 다음에 IO하는 것이 아니라 버퍼링 해서 사용하는 듯 -> 실제 확인 결과 disk,cpu 사용량 거의 100%까지 증가하나 메모리는 크게 변동 사항 없음. 네트웤 사용량은 당연하게 증가
https://pwdd.github.io/post/improving-memory-usage-of-a-java-server
https://www.baeldung.com/java-read-lines-large-file
MobileFaceSwap: A Lightweight Framework for Video Face Swapping (AAAI 2022)
해당 논문에서 제시하는 방식으로 deep fake 적용해봄
-> 22초 동영상 처리하는데 GPU 없이 평균적으로 1분 20초 정도 걸림
-> GPU 이용하면 훨씬 단축 할 수 있을 것 같음
-> 적용 결과 나쁘지 않은 결과. 하지만 실제로 서비스하기엔 2% 부족함.
본격적으로 요구사항 바탕으로 DB 및 API 설계
DB에서 태그 TABLE을 어떻게 구성해야할지 고민을 많이함.
우리 서비스 같은 경우는 일단 인스타 태그와는 달리 계층화된 태그이기 때문에 이런 경우에는 어떻게 DB를 구성해야할지 고민을 많이 함.
-> 네이버 쇼핑에서는 태그를 어떻게 다는지 참고함. -> 네이버 쇼핑 태그는 각 계층 태그가 의존적이지 않아서 우리 서비스 태그와는 조금 결이 다르다고 판단.
-> 다른 테이블에서 태그를 FK로 가지고 있는 경우가 많아서 조회 방식 또한 생각을 안 할 수가 없었음
첫 번째 방식
대TAG - 중TAG - 소TAG 이런 식으로 TAG TABLE을 3개 만들어서 하는 방안
-> 하지만 이 방식은 조회할 때 DB를 3개 DB를 조인해야 하고 ,확장성이 부족하다고 판단함. 예를 들어. TAG 분류가 소 TAG 말고 그 밑에 더 필요하다면?
선택한 방식
1개의 TAG 테이블을 사용하고 TAG_ID, PARENT_TAG_ID를 두어 내 부모 TAG정보를 함께 저장하는 방식을 사용.
-> STACKOVERFLOW나 다른 개발자 커뮤니티에서 계층 태그를 설계할 때 PARENT_TAG_ID를 둔다고 함. 우리 서비스랑도 잘 맞아서 이 방식을 채택함.