250316 회고 💬

와! 정보처리기사 결과가 이번 주에 나왔다. 결과는 예상한대로 합격! 🙌🙌🙌 아직 실기를 취득한건 아니지만 첫 단추를 잘 끼웠다는 의미에서 나 자신 칭찬한다. 4월 중에 실기 시험이 있다. 주말마다 공부해서 한번에 합격할 수 있도록 해보겠다. 어느덧 3월도 중순이다. 3월의 셋째 주를 되돌아본다.

Keep 👍

  • SSAFY 에서 DB 공부를 했다. 대학교 수업으로 들어보곤 몇년만인지,,, 조인, 서브쿼리 등 오랜만에 보는 쿼리문에 대략 정신이 아득해졌다. 서브쿼리는 뭐 그렇구나 하고 이해하기 쉬웠다. 그런데 조인은 공부가 필요할 듯 싶다. 그간 JPA만 사용해서 DB 공부에 소홀했었다. 그러다가 조인 내용을 공부하니 이해할 수가 없었다. 😱😱
  • 자바 io 공부를 하고있다. CS 공부할 때 알아둔 내용들이 몇몇 있어서 복습겸 잘 듣고 있다. 문자집합 같은 단어가 나오면 내가 그간 공부를 하긴 했구나 하면서 기분이 좋다. 자바 io는 개인 프로젝트 할 때 사용해본적이 없다... 알고리즘 공부할 때만 인풋 받으려고 써봤는데 이번 기회에 공부도 했으니 관련해서 기능을 하나 만들어볼까 생각만 하고 있다.
  • 개인 프로젝트에 메서드기반 인증 로직을 구현했다. 지금까지는 경로 기반으로 인증을 확인했었다. 작업하면서 생각해봤는데 경로 기반으로 인증을 구분하면 모든 경로를 securityConfig에 알려줘야한다. 지금은 괜찮지만 나중에 api가 1000개(이렇게 많아지긴 쉽지 않겠지만)가 된다면 과연 그 클래스를 관리할 수 있을까 걱정이 됐다. 그래서 메서드 기반 인증으로 인증인가를 확인하기로 했다.
  • 비디오 스트리밍 기능을 완성했다. 지금까지는 비디오를 요청하면 비디오 재생시 원하는 부분부터 재생하는 기능이 없어서 처음부터 끝까지 다 봐야만 했다. StreamingResponseBody를 사용해서 클라이언트가 비디오의 중간 재생을 원하면 해당 부분부터 스트리밍 하는 식으로 구현했다.
  • 다른 기능으로는 개인 프로필 관리 기능을 만들고 있다. 회원의 프로필 사진, 배경 사진, 자기 소개 등을 관리하는 기능이다. 아직은 백엔드만 완료됐다. 그리고 프론트를 해야하는데,,, 손이 안 간다. 그래서 레스트 독스를 만들어보고 있다.
  • 서비스 단 코드에 로직을 모두 넣어보려고 했지만 그러면 시각적으로 너무 힘들다. 그래서 유틸 클래스로 분리해서 해당 로직을 모두 위임하고 있다. 유틸 클래스를 만들 때 static으로 만들지, @Component로 만들지 고민을 많이 했다. 결론은 @Component 를 사용해서 스프링 빈으로 관리하기로 결정했다. 나중에 테스트 코드 작성할 때 모킹하려면 빈으로 관리하는게 편하기 때문이다. 나는 테스트 코드 작성하는거 좋아하니까,,, 후욱후욱 벌써부터 테스트 코드 고도화 할 생각에 설레기 시작한다.🥰
테스트 코드 고도화 예정
RestDocs -> mockMvc test  
UtillClass -> mockito test  
Repository -> DataJpaTest  
Service, controller -> SpringBootTest
  • 백엔드
    - 메서드 기반 인증인가 체크 로직
    - 비디오 스트리밍 로직 개선
    - 프로필 관리 기능

Try 🧚

  • 토이 프로젝트 작업
    - 백엔드
    - 레스트 독스 구현
    - 테스트 코드 고도화
    - 비디오 관리 기능
    - 프론트엔드
    - 개인 프로필 화면 제작
    - 비디오 관리 기능 화면

독서 목록

서평 완료 목록

서평 예정 목록 (읽는 중)

  • 면접을 위한 CS 전공지식 노트

독서 예정 목록

  • 오브젝트
  • HTTP 완벽 가이드
  • 자바/스프링 개발자를 위한 실용주의 프로그래밍
  • 모던 자바 인 액션
  • 자바 성능 튜닝 이야기
  • 헤드 퍼스트 서블릿
  • 파이브 라인스 오브 코드

Extras 😀

비디오 스트리밍 유틸

@Component
public class VideoStreamingUtil {

	public VideoStreamingResponse resolve(Video video, HttpServletRequest request) {
		// Range 헤더 파싱
		String rangeHeader = request.getHeader("Range");
		long start;
		long end = video.getVideoSize() - 1;

		if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
			String[] ranges = rangeHeader.substring("bytes=".length()).split("-");
			start = Long.parseLong(ranges[0]);
			if (ranges.length > 1 && !ranges[1].isEmpty()) {
				end = Long.parseLong(ranges[1]);
			}
			if (end >= video.getVideoSize()) {
				end = video.getVideoSize() - 1;
			}
		} else {
			start = 0;
		}

		long contentLength = end - start + 1;

		// Response 헤더 설정
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.parseMediaType("video/mp4"));
		headers.add("Accept-Ranges", "bytes");
		headers.setContentLength(contentLength);

		if (rangeHeader != null) {
			headers.set("Content-Range", String.format("bytes %d-%d/%d", start, end, video.getVideoSize()));
		}


		StreamingResponseBody streamingResponseBody = createStreamingResponse(video, start, contentLength);

		// Range 요청이 있으면 206, 없으면 200 반환
		HttpStatus status = (rangeHeader != null) ? HttpStatus.PARTIAL_CONTENT : HttpStatus.OK;
		return new VideoStreamingResponse(streamingResponseBody, status, headers);
	}

	// 스트리밍 바디 생성
	private StreamingResponseBody createStreamingResponse(Video video, long start, long contentLength) {
		return outputStream -> {
			try (RandomAccessFile randomAccessFile = new RandomAccessFile(new File(video.getVideoPath()), "r")) {
				randomAccessFile.seek(start);
				byte[] buffer = new byte[1024];
				long bytesToRead = contentLength;

				while (bytesToRead > 0) {
					int bytesRead = randomAccessFile.read(buffer, 0, (int) Math.min(buffer.length, bytesToRead));
					if (bytesRead == -1) break;
					outputStream.write(buffer, 0, bytesRead);
					bytesToRead -= bytesRead;
				}
				outputStream.flush();
			} catch (IOException e) {
				throw new RuntimeException("Video streaming failed", e);
			}
		};
	}
}

유저 프로필 관리 유틸

@Component
public class UserUtil {

	public User buildUserWith(UserSignUpRequest userSignUpRequest, PasswordEncoder passwordEncoder) {
		return User.builder()
				.name(userSignUpRequest.username())
				.email(userSignUpRequest.email())
				.password(passwordEncoder.encode(userSignUpRequest.password()))
				.userRole(USER)
				.build();
	}

	public String saveImage(MultipartFile profileImage, User user, UserImageType userImageType) {
		if (!Objects.isNull(userImageType.getImagePathOf(user))) {
			deleteImage(userImageType.getImagePathOf(user));
		}

		String basePath = "myVideos/" + user.getId() + "/images/" + userImageType.getFolderName();
		String fileName = UUID.randomUUID() + "_" + profileImage.getOriginalFilename();
		Path filePath = Paths.get(basePath, fileName);

		try {
			Files.createDirectories(filePath.getParent());
			Files.write(filePath, profileImage.getBytes());
		} catch (IOException e) {
			throw new RuntimeException("프로필 사진 변경에 실패했습니다.", e);
		}
		return filePath.toString();
	}

	private void deleteImage(String imagePath) {
		Path path = Paths.get(imagePath);
		try {
			Files.deleteIfExists(path);
		} catch (IOException e) {
			throw new RuntimeException("이미지 삭제에 실패했습니다.", e);
		}
	}
}
profile
What doesn't kill you, makes you stronger

0개의 댓글

Powered by GraphCDN, the GraphQL CDN