안녕하세요 쿠케님! 강의에 나온 10분의 TTL이 없을시 조회수 증가 관련되서 질문드립니다!' 현 상황에서 게시글 접근시 마다 조회수 증가 API를 호출하고 있습니다. 테이블 정보는 다음과 같습니다. Board 테이블 id(auto-increment) readCount BoardRead 테이블 id(auto-increment) boardId userId 게시글 상세 접근시 마다 호출하는 '조회수 증가 API'의 흐름은 다음과 같습니다. 1. 해당 게시글을 유저가 조회 했는지 검증 (BoardRead 테이블에서 검증) 1-1. 조회 한 이력이 있으면 return; 2. Board 비관적 락 조회 3. Board 테이블 readCount 업데이트 4. BoardRead 테이블 insert Board의 readCount는 게시글 조회수를 나타내고, BoardRead는 '각 사용자는 게시글 1개당 1번 조회수 증가'를 검증하기 위한 용도 입니다. '각 사용자는 게시글 1개당 1번 조회수 증가' 정책을 반드시 가져가야 한다면 BoardRead 테이블에 있는 데이터도 레디스로 옮겨야 할까요? 그런데 조회수 데이터는 계속해서 쌓일테고 비즈니스에 중요하지 않은 데이터가 레디스 메모리만 차지하는 느낌이 들어서 꺼려지더 라구욤.. 강의 내용대로 TTL을 걸수 밖에 없는건가 고민도 듭니당.. (레디스는 클러스터 환경으로 사용하고 있습니다.) 기능은 그대로 유지하되 비관적 락을 뺄 수 있는 방법이 있을까요? ※ 번외로 트래픽 바로 몰리니까 비관적 락 로직 때문인지 잠금 이슈 나서 디비 바로 터졌버렸네요 하하하 ㅠ 비관적 락을 선호하지 않는 이유를 체감해버렸다.. ※ 뇌 + GPT 갈구니까 아래와 같은 여러 결론이 나왔습니다. 해결책1 1. Redis SET 자료구조로 중복 체크, 최초 조회면 TTL 걸어줌 2. 분산락(Redis)을 걸고, Board 테이블 readCount 업데이트 및 BoardRead Insert 수행 3. 락 해제 해결책2 1. Redis SET 자료구조로 중복 체크, 최초 조회면 TTL 걸어줌 2. 비동기로 Board 테이블 readCount 업데이트(낙관적 락 적용) 및 BoardRead Insert 수행 작성하면서 문득 'readCount를 정규화 할까?' 했는데 스케이링 넘 클것 같네요.. 이유는 테이블 설계를 JPA의 상속을 활용하는 방안으로 했기 때문에 readCount 필드가 '게시판'이란 추상 클래스에 위치해 있습니다. 주저리 주저리 적어봤는데 머릿속에 혼란이 오네요 ㅠㅠ 자기전 마지막 생각 레디스의 incr를 이용해 조회수 관리, 조회수 데이터는 mysql에 주기적으로 백업
안녕하세요. 재밌고 좋은 강의를 해주셔서 감사합니다. 아직 70%정도 보고 있지만 궁금하게 있습니다. 현재 백엔드 개발자로 근무하고 있는데 저희 환경은 aws의 ec2에 직접 올려 백엔드 서버(spring-boot)를 운영하고 있습니다. 점차 쿠버네티스 환경으로 변경하려고 인프라담당자들과 백엔드 개발자들이 스터디 및 환경 구축하고 있는데요. 만약 실무에서 쿠버네티스 환경으로 바뀐다면 개발자들의 로컬 개발 환경은 어떻게 하시는지 궁금합니다. 대부분 자바, 스프링 환경이면 인텔리제이등으로 개발 후 테스트를 해볼텐데 컨테이너에서 다른 개발자들과 다 같은 환경에서 테스트를 해보고 빌드해보고 해야 하지 않는가 싶어서요. 실무에선 어떤식으로 개발자들이 개발하는지 개발, 테스트 , 운영 배포 까지 플로우가 어떻게 되는지 궁금합니다.
프로젝트 생성부분의 마지막 강의 수강중이고, 위와 같이 코드를 작성했을 때 다음과 같은 오류가 납니다 https://drive.google.com/file/d/1g1uPQj8hZvNmWr3u9NBwMTuI_NOZWSKC/view?usp=sharing 프로젝트 파일도 같이 첨부합니다
[질문 템플릿] 1. 강의 내용과 관련된 질문인가요? 예 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예 3. 질문 잘하기 메뉴얼을 읽어보셨나요? 예 [질문 내용] 멤버 삭제 기능을 만들려고 하는데 이렇게 하는게 맞을까요? 한개의 멤버만 받아서 삭제하고 싶은데 어떻게 처리해야 할까요? public boolean deleteProduct(String productName){ List<Product> result = em.createQuery("select p from Product p where p.productName = :productName", Product.class) .setParameter("productName", productName) .getResultList(); return true; } getResultList 로 하니까 여러개 나올거 같은데 한개만 삭제하려면 어떻게 해야 하나요? 그리고 em.remove하면 되나요?
RouteLocator gatewayRoutes(RouteLocatorBuilder builder) 이 메소드를 만들기 위해 implementation 'org.springframework.cloud:spring-cloud-starter-gateway' 이걸로 수정했을 했는데 이후 저 메소드를 주석 처리하고 applicatio.yml을로 설정을 대체 했을 때는 동작을 안하고 implementation 'org.springframework.cloud:spring-cloud-starter-gateway-mvc' mvc로 다시 변경해줘야 올바르게 동작하는데 무슨 차이가 있을까요?
[질문 내용] 강의에서 회원의 이름으로 중복된 계정을 검증하고, DB에서 회원의 이름을 Unique 제약 조건으로 잡는 게 좋다고 하셨는데 이건 이 예제에서의 가정인 거고, 현업에선 이름만으로 중복 계정을 검증하거나, 이름에 유니크 제약 조건을 거는 경우는 거의 없다고 봐도 되나요?
저는 컴터 사양이 꽤나 좋아서 인지? 항상 표준출력이 더 빠르더군요. 알아보니 로깅 프레임워크가 느린 이유에는 대략 이런 이유들이 있네요. 로그 출력에 여러 과정을 거침. Logger → Appender → Encoder → Layout → OutputStream 레벨 설정에 의한 필터링 로그 포맷팅 처리 분명 표준출력이 synchronized 에 의해서 멀티 스레드 환경에서 취약한건 맞으나 현재 테스트에서는 그 부분이 로깅 프레임워크의 처리 과정보다는 빨랐던 것 같습니다. 테스트 환경 변경 그래서 테스트 환경을 바꿔서 테스트 해보았습니다. 멀티스레드 환경. 스레드 10개. 각 스레드별 100000번 출력 package kr.co.shortenurlservice.log; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Slf4j public class SoutVsAsyncLoggingTest { private static final int THREAD_COUNT = 10; private static final int LOG_COUNT = 100_000; public static void main(String[] args) throws InterruptedException { // 1. System.out.println() 테스트 long sysoutTime = runTest(i -> System.out.println("[SYSOUT][" + Thread.currentThread().getName() + "] " + i) ); // 2. log.info() (AsyncAppender + Console) 테스트 long asyncLogTime = runTest(i -> log.info("[LOGGER][{}] {}", Thread.currentThread().getName(), i) ); // 결과 출력 Thread.sleep(5000); System.out.printf("System.out: %,d ms%n", sysoutTime); System.out.printf("Async log : %,d ms%n", asyncLogTime); } private static long runTest(StringConsumer task) throws InterruptedException { long start = System.currentTimeMillis(); ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT); CountDownLatch latch = new CountDownLatch(THREAD_COUNT); for (int t = 0; t < THREAD_COUNT; t++) { executor.submit(() -> { for (int i = 0; i < LOG_COUNT; i++) { task.accept(i); } latch.countDown(); }); } latch.await(); executor.shutdown(); long end = System.currentTimeMillis(); return end - start; } @FunctionalInterface interface StringConsumer { void accept(int value); } } 또한 test/resources/logback.xml 설정으로 포맷을 날짜 같은것 제외하고 표준출력과 똑같이 맞춰주고 AsyncAppender 를 통해 멀티 스레드 환경에서 차별점이 있도록 비동기 Appender를 추가해줌. <configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%msg%n</pattern> </encoder> </appender> <appender name="ASYNC_CONSOLE" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="CONSOLE"/> <queueSize>10240</queueSize> </appender> <root level="info"> <appender-ref ref="ASYNC_CONSOLE"/> </root> </configuration> 이렇게 하니까 표준출력보다 빠르더군요. 꼭 성능문제뿐 아니라 로그 관리체계 자체가 로깅 프레임워크가 넘사이기 때문에 안쓸 비교불가겠지만.. 성능적으로 보더라도 실무 환경이라면 메시지 출력 전후로 비즈니스 로직들이 있기 때문에 임계영역을 통해 스레드를 대시시키는 표준출력을 사용하는것보다는 로깅 프레임워크가 더욱 성능적으로 이점이 있을 것 같습니다.
vscode를 통해 npm start를 한 후, todo/read/숫자를 입력하니까 404에러가 발생했습니다. 그런데 vscode에서 컨트롤S를 눌렀더니 화면이 떴는데 익스플로러 상에서 새로고침을 하니까 다시 404에러가 발생하는데 이유를 모르겠습니다. 이미지를 첨부할 테니 혹시 더 필요하신 게 있으시면 답변해 주시면 감사드리겠습니다.
안녕하세요! 너무 좋은 무료강의 감사합니다:) 강의를 듣는와중에 궁금증이 생겼는데요. log.info("3) Service virtual true: {}", Thread.currentThread().isVirtual()); 현재 스레드가 가상스레드가 맞는지 확인 하는 위와 같은 코드를 심어 실행해 보았습니다. 컨트롤러의 경우 항상 true를 리턴했고, SimpleAsyncTaskExecutor의 경우에도 true를 리턴했습니다. 그러나 예제코드에서 세팅하신 ThreadPoolTaskExecutor의 설정으로는 false를 리턴하였습니다. 해당 현상을 gpt한테 100% 공식문서 기반으로 설명하라고 하자 ThreadPoolTaskExecutors는 VirtualThread와 상호작용 중 이라는 힌트 정보를 붙인 것 이다(?) 라는 답변을 하여 이해하기가 힘들었습니다. 일단 threadPoolTaskExecutor.setThreadFactory(Thread.ofVirtual().name("vthread-", 0).factory()); 와 같은 ThreadPoolTaskExecutors의 세팅을 추가해주니 isVirtual 에대한 true값이 나오긴 했습니다. 제 부족한 견문으로는 이해가 되지 않는 현상이라 강사님이 도움을 주실수 있나해서 문의드립니다 🙂
안녕하세요. 쿠케님 강의 너무나 잘 보고 있습니다. 쿠케님 질의응답 게시글 정독하고 있는데 1:1 멘토링 부럽지 않을만큼의 고퀄리티 답변 언제나 감사드립니다. (갬동갬동) 현재 회사에서 조회수를 RDB에만 저장하고 있습니다. 이를 강의 내용처럼 'Redis 이전 & MySql 백업'을 적용 하고 싶은데 아래 흐름대로 적용 하면 될까요? 1⃣ (개발) redis 저장 및 조회, mysql 백업 코드 작성 2⃣ (개발) 기존 MySQL 데이터를 Redis에 저장하는 마이그레이션 코드 작성 => 이렇게 일회성 마이그레이션 같은 경우 테스트 코드로 한번만 돌리는데 쿠케님은 어떤 방식으로 하시나요? 3⃣ (배포 전) 1번 코드 배포 직전에 2번 로직 실행 4⃣ (배포) 1번 코드 배포 위 방식에서 발생할 수 있는 문제점 1. 3번(마이그레이션)과 4번(배포) 사이에 조회수가 누락될 가능성 - 마이그레이션 실행 후 MySQL에는 새로운 조회수가 계속 업데이트 되지만, Redis는 아직 트래픽을 받지 않음. - 즉, 마이그레이션 실행 이후 MySQL에 새로 기록된 조회수는 Redis에 반영되지 않음 → 데이터 불일치 발생 가능. 2. 처음 Redis로 전환할 때, Redis에 캐싱되지 않은 일부 조회수가 MySQL에 계속 쿼리될 가능성이 있음. - 처음 Redis로 전환할 때, Redis에 캐싱되지 않은 일부 조회수가 MySQL에 계속 쿼리될 가능성이 있음. - 특정 조회수가 빠르게 증가하면 Hot Key 이슈 발생 가능. 조회수 누락을 최소화 하는 방법 1⃣ (개발) redis 저장 및 조회, mysql 백업 코드 작성 2⃣ (개발) 기존 MySQL 데이터를 Redis에 저장하는 마이그레이션 코드 작성 및 실행 3⃣ (1번 코드 배포 전) 더블 라이트(Double Write) 모드 개발하여 운영에 적용 4⃣ (배포) 기존 MySQL 기반 조회수 코드 제거, Redis 기반으로 전환 5⃣ (배포 후) MySQL 백업 로직 실행 및 기존 MySQL 조회 로직 완전히 제거 제가 혹여나 놓친게 있거나 더 좋은 방법이 있으면 천천히 답변 부탁드립니닷! 출처: 내 뇌 + GPT 센세
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요. 1. 강의 내용과 관련된 질문을 남겨주세요. 2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요. (자주 하는 질문 링크: https://bit.ly/3fX6ygx) 3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요. (질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG) 질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요. ========================================= [질문 템플릿] 1. 강의 내용과 관련된 질문인가요? (예/아니오) 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오) 3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오) [질문 내용] 여기에 질문 내용을 남겨주세요 혹시 강사님 코드를 복붙해도 이상태인데 왜 이러는걸까요ㅠㅠ 저 springdpa말고는 다 작동 되었습니다! .
FeignErrorDecoder 에 대해서 order_is_empty를 수정한뒤에 127.0.0.1:8000/actuator/busrefresh 를 호출하면 Keys refreshed [ spring.cloud .bootstrap.enabled, order_service.exception.order_is_empty] 로그는 정상적으로 뜨며 디버깅 모드로 코드를 추가해서 실행한결과 List<ResponseOrder> orders= orderServiceClient.getOrders(userId); 코드전에 env.getProperty("order_service.exception.order_is_empty") 를 확인해본 결과는 정상적으로 리프레시되지만 이후에 오류를 터트리고 FeignErrorDecoder 처리 로직에서 switch(response.status()){ case 400: break; case 404: if(methodKey.contains("getOrders")){ return new ResponseStatusException(HttpStatus.valueOf(response.status()),env.getProperty("order_service.exception.order_is_empty")); } break; default: return new Exception(response.reason()); } getProperty("order_service.exception.order_is_empty") 이부분이 새로운 환경 변수가 ErrorDecoder 에서는 반영 안되는것같아서 해결이 안되서 질문남깁니다 ! UserServiceImpl 에서는 정상적으로 바뀐값이 나오는거보면 ErrorDecoder 문제 같은데 정확히 모르겠습니다 @RefreshScope 이것도 해결책은 아닌것같아요 답변 부탁드립니다 !
ReadComponent.js 파일을 만들고 ReadPage의 리턴 내용에 ReadComponent의 tno 값을 추가한 후에 npm start를 실행했는데 아래와 같은 에러가 발생했습니다. [eslint] src\components\menus\todo\ReadComponent.js Line 13:29: 'useState' is not defined no-undef Line 16:9: 'getOne' is not defined no-undef Search for the keywords to learn more about each error. ReadComponent.js의 useState와 getOne 로직 부분이 잘못 된 거 같아서 강의 영상을 다시 봤는데 틀린 부분이 없어 보여 어떤 게 문젠지 질문을 드리고 싶습니다. 참고용으로 ReadComponent.js의 내용을 추가해놓겠습니다. import React, { useEffect } from 'react'; const initState = { tno:0, title:'', writer:'', dueDate: '', complete: false } function ReadComponent({tno}) { const [todo, setTodo] = useState(initState) useEffect(() => { getOne(tno).then(data => { console.log(data) setTodo(data) }) }, [tno]); return ( <div> </div> ); } export default ReadComponent; 덧붙여서 툴은 vscode를 쓰고 있습니다. 잘 부탁드리겠습니다.
안녕하세요. 강의 잘 듣고 있습니다. 제가 원래 질문이 많은데 개념을 이해가 잘 되도록 쉽게 설명해 주셔서 질문 드릴게 별로 없네요. 보통 api path에 id를 추가하시는거 같은데요. 현업에서도 auto_increment나, snowflake id같은 db에서 쓰는 id를 그대로 넣고 사용하시나요? 아니면 prefix등을 추가하여 조금 더 가공을 한다든지 하시나요? 숫자만 들어가니 좀 밋밋해 보이기도 하고 알아보기도 힘들거 같기도 해서요.
강의 내용 중에 POST 와 같이 데이터를 변경하는 요청의 경우 csrf 토큰을 생성한다는 내용이 있는데 로그인 페이지 요청시 GET 으로 요청하는데 csrf 토큰이 input 태그에 포함되어 있는 걸로 보아 POST 와 같이 데이터틀 변경하는 요청의 경우 csrf 토큰을 생성하는 게 아니라 csrf 토큰을 검증하는 것이고, csrf 토큰은 모든 요청에 대해 반환해준다가 맞을까요? 또 csrf 토큰이 난수를 조합해 계속 변경해서 클라이언트에 반환한다고 들었는데 제가 잘못 본 건지 모르겠지만 토큰을 디코딩할 때 난수를 조합하는 코드를 보면 토큰의 길이로 난수를 만드는 것 같은데, 토큰의 길이로 난수를 만든다고 하면 토큰을 변하게 하지 않는 이상 인코딩된 토큰의 결과가 똑같을 것 같거든요. 그래서 세션당 고유 토큰 값은 똑같다고 한다면 로그인 전에는 클라이언트에 반환하는 토큰이 계속 바뀌고 (=익명 사용자 세션이니) 로그인 이후에는 클라이언트에 반환하는 토큰이 일정한 게 아닐까 하는 물음이 생겼습니다. 어느게 맞는 걸까요 ?
안녕하세요 오라클과 jpa를 사용중인데요.. 엔터티에서 id 지정할 때 .generatedValue.identity 옵션이 적용이 불가합니다 그래서 auto나 다른 옵션으로 주면 테이블의 아이디값이 무작위로 나오는거 같은데요 mysql처럼 1부터 순서대로 나오게 하고 싶으면 어떻게 해야 할까요?.. 아시는 분은 답변 주시면 감사하겠습니다