제품 자체는 앱의 기본 flow만 갖춰진 상태다. 앱 스플래시 - 로그인 - 홈 진입 후 검색 / 즐겨찾기 / 주문내역 / 카테고리 페이지를 볼 수 있고 restaurant -> food_detail -> cart -> payment -> order_success로 이어지는 funnel을 갖고있다. WITH login_data AS ( SELECT DISTINCT user_pseudo_id, FORMAT_DATE('%Y-%m-01', event_date) AS login_month FROM advanced.app_logs WHERE event_date BETWEEN '2022-03-01' AND '2023-01-31' ) SELECT login_month, COUNT(DISTINCT user_pseudo_id) FROM login_data GROUP BY login_month ORDER BY login_month MAU를 계산해보면, 7,500에서 시작하여 23,373까지 성장했다. 23년 1월 데이터는 절반 정도까지만 있으므로, 지속적인 성장세를 보여주고 있다고 판단할 수는 있겠다. 하지만 런칭 후 +2달 이후로는 계속 진전 없는 모습을 보여준다. 배달 앱이기 때문에, 평일-주말에는 다른 접속률을 보여줄 것이다. 그러므로 weekly retention 지표를 활용한다. WITH base AS( SELECT DISTINCT user_id, event_name, DATETIME(TIMESTAMP_MICROS(event_timestamp), 'Asia/Seoul') AS event_datetime, DATE(TIMESTAMP_MICROS(event_timestamp), 'Asia/Seoul') AS event_date, user_pseudo_id FROM advanced.app_logs WHERE event_date BETWEEN '2022-08-01' AND '2022-11-03' AND event_name = 'click_payment' ) , dates AS( SELECT DISTINCT user_pseudo_id, DATE_TRUNC(MIN(event_date) OVER (PARTITION BY user_pseudo_id), WEEK(MONDAY)) AS first_week, DATE_TRUNC(event_date, WEEK(MONDAY)) AS event_week FROM base ) , first_week_and_diff AS ( SELECT *, DATE_DIFF(event_week, first_week, week) AS diff_of_week FROM dates ) , user_counts AS ( SELECT diff_of_week, COUNT(DISTINCT user_pseudo_id) AS user_cnt FROM first_week_and_diff GROUP BY diff_of_week ORDER BY diff_of_week ) , first_week_user_count AS ( SELECT diff_of_week, user_cnt, FIRST_VALUE(user_cnt) OVER(ORDER BY diff_of_week) AS first_week_user_cnt FROM user_counts ) SELECT *, SAFE_DIVIDE(user_cnt, first_week_user_cnt) AS retain_rate FROM first_week_user_count weekly retention으로 살펴보면, 첫 주에 1%의 고객이 남고 그 후로는 어느정도 플래튜가 형성되는 모습이다. 따라서 아직은 PMF를 찾지 못했다고 할 수 있다. 그렇다면 재접속을 하는 유저는 그렇지 않은 유저와 어떻게 다를까? 총 기간동안의 퍼널 이벤트 수는 아래와 같다. WITH base AS ( SELECT event_date , event_timestamp , event_name , user_id , user_pseudo_id , platform , MAX(IF(event_param.key = 'firebase_screen', event_param.value.string_value, NULL)) AS firebase_screen , MAX(IF(event_param.key = 'session_id', event_param.value.string_value, NULL)) AS session_id FROM `advanced.app_logs` CROSS JOIN UNNEST(event_params) AS event_param WHERE event_date BETWEEN '2022-08-01' AND '2023-01-31' GROUP BY ALL ) , filter_event_and_concat_event_and_screen AS ( SELECT * EXCEPT(event_name, firebase_screen, event_timestamp) , DATETIME(TIMESTAMP_MICROS(event_timestamp), 'Asia/Seoul') AS event_datetime , CONCAT(event_name, '-', firebase_screen) AS event_name_with_screen FROM base WHERE event_name IN ('screen_view', 'click_payment') ) SELECT event_name_with_screen , CASE WHEN event_name_with_screen = 'screen_view-welcome' THEN 1 WHEN event_name_with_screen = 'screen_view-home' THEN 2 WHEN event_name_with_screen = 'screen_view-food_category' THEN 3 WHEN event_name_with_screen = 'screen_view-restaurant' THEN 4 WHEN event_name_with_screen = 'screen_view-cart' THEN 5 WHEN event_name_with_screen = 'click_payment-cart' THEN 6 ELSE NULL END AS STEP_NUMBER , COUNT(DISTINCT user_pseudo_id) AS cnt FROM filter_event_and_concat_event_and_screen GROUP BY ALL HAVING step_number IS NOT NULL ORDER BY step_number 홈에서 food category를 선택하는 유저의 비율은 65% 카테고리에서 식당을 고르는 유저의 비율은 91% 식당에서 장바구니에 담는 유저의 비율은 58% 장바구니로부터 결제하는 유저의 비율은 67% 정도이다. 일단 탐색을 시작하면 카테고리, 식당에 대해 잘 찾아보는 편이고, 장바구니에 담아 결제까지 하는 것에 대해서는 event 수가 급감한다. 재접속을 하는 유저들의 퍼널을 페이지 기준으로 나눠보았다. WITH base AS( SELECT DISTINCT user_id, event_name, DATETIME(TIMESTAMP_MICROS(event_timestamp), 'Asia/Seoul') AS event_datetime, DATE(TIMESTAMP_MICROS(event_timestamp), 'Asia/Seoul') AS event_date, user_pseudo_id FROM advanced.app_logs WHERE event_date BETWEEN '2022-08-01' AND '2023-01-31' ) , dates AS( SELECT DISTINCT user_pseudo_id, DATE_TRUNC(MIN(event_date) OVER (PARTITION BY user_pseudo_id), WEEK(MONDAY)) AS first_week, DATE_TRUNC(event_date, WEEK(MONDAY)) AS event_week FROM base ) , first_week_and_diff AS ( SELECT *, DATE_DIFF(event_week, first_week, week) AS diff_of_week FROM dates ) , retain_user AS ( SELECT DISTINCT user_pseudo_id FROM first_week_and_diff WHERE diff_of_week >= 1 ) , base2 AS ( SELECT event_date , event_timestamp , event_name , user_id , user_pseudo_id , platform , MAX(IF(event_param.key = 'firebase_screen', event_param.value.string_value, NULL)) AS firebase_screen , MAX(IF(event_param.key = 'session_id', event_param.value.string_value, NULL)) AS session_id FROM `advanced.app_logs` CROSS JOIN UNNEST(event_params) AS event_param WHERE event_date BETWEEN '2022-08-01' AND '2023-01-31' AND user_pseudo_id IN ( SELECT user_pseudo_id FROM retain_user ) GROUP BY ALL ) , filter_event_and_concat_event_and_screen AS ( SELECT * EXCEPT(event_name, firebase_screen, event_timestamp) , DATETIME(TIMESTAMP_MICROS(event_timestamp), 'Asia/Seoul') AS event_datetime , CONCAT(event_name, '-', firebase_screen) AS event_name_with_screen FROM base2 WHERE event_name IN ('screen_view', 'click_payment') ) SELECT event_name_with_screen , CASE WHEN event_name_with_screen = 'screen_view-welcome' THEN 1 WHEN event_name_with_screen = 'screen_view-home' THEN 2 WHEN event_name_with_screen = 'screen_view-food_category' THEN 3 WHEN event_name_with_screen = 'screen_view-restaurant' THEN 4 WHEN event_name_with_screen = 'screen_view-cart' THEN 5 WHEN event_name_with_screen = 'click_payment-cart' THEN 6 ELSE NULL END AS STEP_NUMBER , COUNT(DISTINCT user_pseudo_id) AS cnt FROM filter_event_and_concat_event_and_screen GROUP BY ALL HAVING step_number IS NOT NULL ORDER BY STEP_NUMBER 한번이라도 재접속한 사람들은 웰컴 -> 홈을 넘어가는 비율이 거의 100%에 가깝다. 아무래도 login이란 허들때문일 것이다. 그렇다면, login 허들을 좀 더 잘 넘기 위해 제품 온보딩을 잘 해줘야하지 않을까? 확실히 프로덕트에 대한 차별점 설명이 부족하므로, 이 부분을 강화하여 '어떤 core value를 가진 배달앱'인지를 어필할 필요가 있겠다. 그리고 홈에서 카테고리로 넘어가는 비율이 73%, 카테고리에서 식당 페이지로 92%, 식당 페이지에서 장바구니로는 55% 장바구니로부터 결제까지 이어지는 비율은 67%이다. 재접속한 유저들은 홈에서 food category를 선택하는 데에 좀 더 높은 전환율을 보였다. 그렇다면 이들이 다른 유저들 대비하여 추가적으로 느낀 이점은 무엇일지 고민할 필요가 있을 것이다. 재접속 기준만으로는 차이가 별로 없어서 click payment 경험이 있고 재접속을 한 유저들의 퍼널을 구해봤다. 비교적 매우 높은 전환율을 보여준다. 특히 카테고리를 굳이 선택하지 않고 바로 식당을 선택하는데, 카테고리가 비교적 메리트있게 설계되지 않아서 그런 것인지? 라는 생각이 든다. 이벤트 데이터가 더 있었다면, 식당으로 전환되는 페이지들의 비율을 살펴보면서 홈의 비중이 높지 않을까 하고 보겠지만, 지금은 그냥 그렇지 않을까 하고 추측해본다. 확실히 구매를 했고, 2번 이상 접속한 유저들의 퍼널 전환율은 매우 좋다. 해당 제품이 직접 보기에는 배달 주문에 필수적인 기능들만 있지만, 해당 1만 4천명 정도의 유저들이 느끼는 분명한 메리트가 있을 것이다. 그 메리트로부터 core value를 찾고, 고객들이 좋아하는 기능 중심으로 발전시켜 나가야할 것이다.
토스페이먼츠의 어떤 프로젝트에서 일부 변수명은 한글로 작성하는 컨벤션이 존재하는거로 알고있습니다 물론 모든 변수를 한글로 하는건 지양하고 영어로 풀어내기 어려운 경우 한글을 사용하는걸로 컨벤션을 작성해놓은거같은데 해당 프로젝트가 자바스크립트의 컨벤션이어서 자바랑은 다른건지 궁금합니다~
강의를 보면서 cmd 실행후 helloC:\Users\Desktop\Study\hello-spring\hello-spring> 에서 gradlew build를 실행하면 해당 에러가 계속 뜨네요. 처음엔 다른문제가 뜨길레 윈도우 계정이름도 변경하고, 버전도 확인하고 cmd관리자 권한으로도 실행해봤는데도 계속 이래요. 공부가 하고 싶습니다 좀 도와주세요 너무 답답합니다 ㅠㅠ
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요. 1. 강의 내용과 관련된 질문을 남겨주세요. 2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요. (자주 하는 질문 링크: https://bit.ly/3fX6ygx) 3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요. (질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG) 질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요. ========================================= [질문 템플릿] 1. 강의 내용과 관련된 질문인가요? (예/아니오) 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오) 3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오) [질문 내용] 안녕하세요, 테스트가 정상적으로 실행되었으며 @Rollback 어노테이션을 붙였음에도 불구하고 왜 데이터가 insert 되지 않는지 궁금합니다. 관련 사진은 다음과 같습니다. 코드는 다음과 같습니다. package jpabook.jpashop; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.transaction.annotation.Transactional; import static org.junit.jupiter.api.Assertions.*; @RunWith(SpringRunner.class) @SpringBootTest class MemberRepositoryTest { @Autowired MemberRepository memberRepository; @Test @Transactional @Rollback(false) public void testMember() throws Exception { // given Member member = new Member(); member.setUsername("memberA"); // when Long saveId = memberRepository.save(member); Member findMember = memberRepository.find(saveId); // then Assertions.assertThat(findMember.getId()).isEqualTo(member.getId()); Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername()); } } spring: datasource: url: jdbc:h2:tcp://localhost/~/jpashop username: sa password: driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: update properties: hibernate: #show_sql: true format_sql: true logging.level: org.hibernate.SQL: debug #org.hibernate.type: trace 감사합니다.
안녕하세요! 엔티티 -> DTO, DTO -> 엔티티 변환 모두 STRICT 전략을 사용하는 modelMapper.map을 사용하고있습니다. DTO를 쓰는 이유 -> 엔티티가 바뀌어도, dto는 그대로이므로 api 스펙이 바뀌지않음. 그런데 STRICT 전략을 쓰는 modelMapper를 사용하면, dto와 엔티티 필드이름이 정확하게 일치해야하므로 엔티티가 바뀌면 dto 필드도 바뀌어야함. 이렇게 되면 엔티티 변경 시에 api 스펙이 바뀌지않는다는 장점이 없어지게됨 이런 생각이 들었는데, 그럼 STRICT 전략은 지양해야하나요, 아님 다른 방법이 있는 것인가요?
안녕하세요. 좋은 강의 항상 감사합니다. JPA 와 디비를 배우면서 적용 하는 과정에서 궁금한게 생겼습니다. @Transactional public Post getPostDetail(Long postId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); post.incrementViewCount(); // 조회수 증가 return post; } 상세글 조회 시 조회수가 +1 되는 간단한 로직을 가정하겠습니다. 트래픽이 크다고 가정 할 시 해당 로직 대로 하면 조회 시 마다 DB에 부하가 예상되서 질문 드립니다. 해당 로직대로 해도 DB에 크게 부하를 주지 않는 수준인가요? Redis를 써서 조회수 만 따로 캐시로 저장을 한다면 DB랑 데이터가 정합하지 않을 것으로 예상됩니다. 만약 조회수가 중요한 서비스라면 db 락을 이용하여 성 능 저하를 감안 하고 하시는지.. 실무에선 어떻게 처리하시는 지 궁금합니다
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요. 1. 강의 내용과 관련된 질문을 남겨주세요. 2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요. (자주 하는 질문 링크: https://bit.ly/3fX6ygx) 3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요. (질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG) 질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요. ========================================= [질문 템플릿] 1. 강의 내용과 관련된 질문인가요? (예/아니오) 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오) 3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오) [질문 내용] 연관관계의 주인은 일반적으로 외래키가 있는 쪽에 설정하는 것이 좋다고 알고 있습니다. 강의자료에서 "일대다 단방향 관계에서는 1 쪽이 연관관계의 주인이 된다." 라는 것은 "항상" 참인건가요? 아니면 강의의 2:11 처럼 Member 가 Team 을 참조할 일이 없는 특수한 경우에는 "1쪽을 연관관계의 주인으로 둘 수도 있다" 라는 것인가요? 만약, Member 엔티티에서 Team 을 참조하는 일대다 양방향인 상황이 생긴다면, "여전히 1쪽을 연관관계의 주인으로 둘 수도 있지만, 장단점을 고려했을때 다쪽을 연관관계의 주인으로 두는 다대일 양방향을 사용하는 것이 좋다 " 라고 이해하였는데 맞는 말일까요?
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요. 1. 강의 내용과 관련된 질문을 남겨주세요. 2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요. (자주 하는 질문 링크: https://bit.ly/3fX6ygx) 3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요. (질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG) 질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요. ========================================= [질문 템플릿] 1. 강의 내용과 관련된 질문인가요? (예/아니오) 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오) 3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오) [질문 내용] 안녕하세요 강의를 듣다가 궁금한게 생겨서 질문 드립니다. 앞에 수업에서는 부모 클래스는 자식 클래스가 누군지 모른다고 했었고 부모가 자식의 기능을 사용하려면 다운 캐스팅을 해서 사용 가능하다고 하셨는데, 이 코드의 메서드 파라미터 부분 (Animal animal)에서 Dog, Cat, Caw가 어떻게 상속 받은 자식 클래스인 걸 알고 작동하는지 궁금합니다ㅠㅠㅠㅠ package poly.ex2; public class AnimalPolyMain1 { public static void main(String[] args) { Dog dog = new Dog(); Cat cat = new Cat(); Caw caw = new Caw(); soundAnimal(dog); soundAnimal(cat); soundAnimal(caw); } //동물이 추가 되어도 변하지 않는 코드 private static void soundAnimal(Animal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } }
불변 객체는 내부 필드에 변경이 생길 경우 새로운 객체를 반환해줘야 한다고 배웠습니다. 제네릭 타입은 타입 소거 문제 때문에 new 키워드를 사용할 수 없는데... 이 경우 제네릭과 불변 객체는 공존할 수 없는 건가요? 아래 코드와 같이 실습을 해봤는데 주석 처리한 부분을 어떻게 해결해야 할지 잘 모르겠습니다 ㅠㅠ public class Box<T extends Animal> { private final T value; public Box(T value) { this.value = value; } public T get() { return value; } public Box<T> withName(String name) { //이 부분을 어떻게 처리해야할까요..? } } public class Animal { private final String name; private final int size; public Animal(String name, int size) { this.name = name; this.size = size; } public Animal withName(String name) { return new Animal(name, size); } public class Dog extends Animal { public Dog(String name, int size) { super(name, size); } @Override public Dog withName(String name) { return new Dog(name, getSize()); } }
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요. 1. 강의 내용과 관련된 질문을 남겨주세요. 2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요. (자주 하는 질문 링크: https://bit.ly/3fX6ygx) 3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요. (질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG) 질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요. ========================================= [질문 템플릿] 1. 강의 내용과 관련된 질문인가요? (예/아니오) 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오) 3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오) [질문 내용] 여기에 질문 내용을 남겨주세요. 안녕하세요. 잘 보고 있습니다. 컨트롤러에서 Repository에 바로 의존성이 있는 방식으로 사용하는 예제를 알려주셨는데 간단한 예시를 들어주시느라 사용하신 패턴이신가요? 아니면 어차피 페이징 기능은 레포지토리 단의 간단한 메서드로 충분히 수행되므로 서비스 레이어를 통한 매핑이 불필요해서 그러신건가요??