반응형

작성한 코드를 어떤식으로 정리 해야할지 몰라서 나름대로 생각해서 정리해보기로 했습니다.


프로젝트 구조

- main/~/project_name // 소스 코드 디렉토리

    - domain // 변수 디렉토리
        - search // 외부 api통신의 데이터를 취득 하거나 내부DB에서 검색과 관련 데이터를 이용할때 사용
            - SearchResult.java // 내부 DB안에 채널을 검색한 결과를 도출할때 사용
            - YoutubeChannelList.java // youtube api( 개인이 구독한 채널에 대한 데이터 )에서 데이터를 입력받을때 사용
            - YoutubeIsLive.java // 클라이언트 서버에서 요청할때 채널에 대한 라이브 여부를 송신하기 위해서 사용
            - YoutubeSearchResult.java // youtube api( 유저에 대한 데이터 )에서 데이터를 입력받을때 사용
        - Follower.java // 구독한 채널에 대한 정보 ( entity ) 
        - LiveConfig.java //  채널에 대한 상세정보 ( entity ) 
        - LoginRequest.java // 클라이언트으로 부터 요청을 받을때 사용하는 폼 ( 로그인 )
        - Member.java // 사용자에 대한 정보 ( entity )
        - SignupRequest.java // 클라이언트으로 부터 요청을 받을때 사용하는 폼 ( 회원가입 )
        - TwitchSearchResult.java // twitch api( 채널검색에 대한 데이터 )에서 데이터를 입력받을때 사용 
    - mappers  // 마이바티스에서 사용할 mapper의 집합 디렉토
        - FollowerMapper.java // 구독한 채널에 대한 DB 기능
        - LiveConfigMapper.java // 입력된 채널들에 대한 DB
        - MemberMapper.java // 유저 정보에 대한 DB 기능
    - repository // DB의 데이터들의 이용방법 디렉토리
        - FollowerRepository.java  // 구독한 채널에 대한 DB 데이터의 기능을 설명하는 추상화
        - FollowerRepositoryV1.java  // jpa를 이용한 기능 구현
        - LiveConfigRepository.java  // crudRepository을 활용한 DB liveconfig의 데이터 활용 기능 구현
        - MemberRepository.java  // 이용자에 대한 DB 데이터의 기능을 설명하는 추상화
        - MemberRepositoryV1.java  // jpa를 이용한 기능 구현
    - service  // 데이터와 상호 작용하는 서비스 디렉토리
        - CheckYoutubeLiveStreamService.java  // 서버내에서 작동하는 채널들의 라이브 판정처리 추상화
        - CheckYoutubeLiveStreamServiceV1.java  // jpa를 이용한 기능 구현
        - CheckYoutubeLiveStreamServiceV2.java  // mybatis를 이용한 기능 구현
        - MemberService.java  // 이용자들이 홈페이지 내에서 이용하는 서비스의 추상화
        - MemberServiceV1.java  // jpa를 이용한 기능 구현
        - MemberServiceV2.java  // mybatis를 이용한 기능 구현
        - SearchService.java  // youtube, twitch api를 이용한 검색에 대한 기능 추상화
        - SearchServiceV1.java  // HTTP 통신을 통한 API 호출 및 응답 처리
    - web  // 클라이언트와의 상호작용 처리 디렉토리
        - BroadcastSearchController.java  // 생방송 관련 클라이언트와 상호작용 처리
        - HomeController.java  // 이용자의 로그인 및 회원가입과 같은 기능들을 클라이언트와 상호작용 처리
    - AsyncConfig.java  // 비동기 처리에 대한 상세정보
    - MybatisConfig.java  // mybatis에 대한 상세정보
    - PersonalApplication.java  // spring앱 메인
    - SchedulingConfig.java  // 스케줄링에 대한 상세정보
    - ServletInitializer.java  // 외부 tomcat 사용을 위한 처리
    - SessionManager.java  // 세션 처리 관련 기능
    - SpringConfig.java  // 앱 기능에 대한 상세정보

- resources  // 정적 파일 디렉토리

    - mappers  // mybatis 맵퍼를 저장하는 디렉토리

        - FollowerMapper.xml  // sql Follower테이블에 접근하는 방법을 정의해놓은 xml

        - LiveConfigMapper.xml  // sql LiveConfig테이블에 접근하는 방법을 정의해놓은 xml

        - MemberMapper.xml  // sql follower테이블에 접근하는 방법을 정의해놓은 xml

    - application.properties  // 하드코딩을 숨기기 위한 properties

    - chromedriver.exe  // 셀레니움을 활용하기 위한 크롬 드라이버

    - mybatis-config.xml  // mybatis 상세 설정

 

의 구조로 백엔드를 구성해 보았다.

V1 하고 V2가 있는데 V1는 일반적인 jpa를 이용한 방법으로 만들어 보았고 V2는 mybatis를 이용한 방법으로 코드를 만들어 보았다.

 

※ 패키지명은 기입하지 않음

@Setter @Getter
public class SearchResult{
    private LiveConfig liveConfig;
    private boolean searchResult;
}

 

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class YoutubeChannelList {
    private String nextPageToken;
    private List<Items> items;
    
    @JsonIgnoreProperties(ignoreUnknown = true) @Data
    public static class Items{
        private Snippet snippet;

        @JsonIgnoreProperties(ignoreUnknown = true) @Data
        public static class Snippet {
            private String title;
            private String customUrl;
            private String description;
            private String channelTitle;
            private Thumbnails thumbnails;
        }

        @JsonIgnoreProperties(ignoreUnknown = true) @Data
        public static class Thumbnails {
            private Image medium;
        }

        @JsonIgnoreProperties(ignoreUnknown = true) @Data
        public static class Image {
            private String url;
        }
    }
}

 

jackson의 기능

@JsonIgnoreProperties(ignoreUnknown = true) 의 경우는 파싱할때 필요한 데이터만 받기 위해서 입력함 

 

@Data
public class YoutubeIsLive {
    private String title;
    private String isLive;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class YoutubeSearchResult {
    private List<Items> items;
    private String nextPageToken;
    private PageInfo pageInfo;
    
    @JsonIgnoreProperties(ignoreUnknown = true) @Data
    public static class Items{
        private Snippet snippet;
        private ContentDetails contentDetails;

        @JsonIgnoreProperties(ignoreUnknown = true) @Data
        public static class Snippet {
            private String channelTitle;
            private Thumbnails thumbnails;
        }

        @JsonIgnoreProperties(ignoreUnknown = true) @Data
        public static class Thumbnails {
            private Image medium;
        }

        @JsonIgnoreProperties(ignoreUnknown = true) @Data
        public static class Image {
            private String url;
        }

        @JsonIgnoreProperties(ignoreUnknown = true) @Data
        public static class ContentDetails {
            private Subscription subscription;
        }

        @JsonIgnoreProperties(ignoreUnknown = true) @Data
        public static class Subscription {
            private ResourceId resourceId;
        }

        @JsonIgnoreProperties(ignoreUnknown = true) @Data
        public static class ResourceId {
            private String channelId;
        }
    }

    @JsonIgnoreProperties(ignoreUnknown = true) @Data
    public static class PageInfo {
        private int totalResults;
    }
}
@Entity
@Data
public class Follower {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "twitch_channel_id")
    private String twitch_channel_id;

    @ManyToOne
    @JoinColumn(name = "member_id", referencedColumnName = "id")
    private Member member;

    @ManyToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "custom_url", referencedColumnName = "custom_url")
    private LiveConfig liveConfig;
}
@Entity
@Data
public class LiveConfig {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String name;
    private String description;
    @Column(name = "custom_url")
    private String custom_url;
    @Column(name = "thumbnails_url")
    private String thumbnails_url;

    @Column(name = "is_live")
    private String is_live;
    @Column(name = "video_id")
    private String video_id;
    @Column(name = "is_check")
    private boolean is_check;
}
@Data
public class LoginRequest {
    private String username;
    private String password;
}
@Entity
@Data
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "username", unique = true)
    private String username;
    @Column(name = "password")
    private String password;
    @Column(name = "youtube_channel_id") 
    private String youtubeChannelId;
    @OneToMany(mappedBy = "member", cascade = CascadeType.PERSIST)
    private List<Follower> followers;
}

아이디(username)가 겹치는건 말이 안되기 때문에 unique 속성을 넣었고 followers를 수정하면서 jpa에서 persist할때 자동으로 같이 입력이 되기를 바라기 때문에 cascade = CascadeType.PERSIST 를 추가하였습니다.

 

@Data
public class SignupRequest {
    private String username;
    private String password;
    private String youtubeChannelId;
}

회원가입 호출을 받을 시 입력받는 데이터의 종류

 

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class TwitchSearchResult {
    private List<data> data;

    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class data {
        private String broadcaster_login;
        private String display_name;
        private String is_live;
        private String thumbnail_url;
    }
}

 

@Mapper
public interface FollowerMapper {
    void save(@Param("memberId") Long memberId, @Param("customUrl") String customUrl);
    void delete(Member member, @Param("customUrl") String customUrl);
    void updateTwitchChannelId(@Param("twitch_channel_id") String twitch_channel_id, @Param("member_id") Long member_id, @Param("custom_url") String customUrl);
    Follower findFollower(Member member, @Param("customUrl") String customUrl);
    List<String> findAllFollowerByMemberId(@Param("memberId") Long memberId);
}

 

@Mapper
public interface LiveConfigMapper {
    void save(LiveConfig liveConfig);
    void updateLiveConfig(LiveConfig liveConfig);
    void setByIsCheckFalseAll();
    void delete(LiveConfig liveConfig);
    LiveConfig findByCustomUrl(String custom_url);
    int existsByCustomUrl(String custom_url);
    int countByIsCheckFalse();
    List<LiveConfig> findAll();
    List<LiveConfig> findByIsCheckFalse();
}
@Mapper
public interface MemberMapper {
    void save(Member member);
    void delete(Member member);
    Member findMember(String username);
}
public interface FollowerRepository {
    Follower saveFollower(Follower follower);
    Follower deletFollower(Follower follower);
    Follower findFollower(Member member, String customUrl);
}
public class FollowerRepositoryV1 implements FollowerRepository{

    @PersistenceContext
    EntityManager em;

    @Override
    public Follower saveFollower(Follower follower) {
        em.persist(follower);
        return follower;
    }

    @Override
    public Follower deletFollower(Follower follower) {
        em.remove(follower);
        return follower;
    }

    @Override
    public Follower findFollower(Member member, String customUrl) {
        Follower follower = em.createQuery("SELECT e FROM Follower e WHERE e.custom_url = :customUrl AND e.member.id = :memberId", Follower.class).
        setParameter("customUrl", customUrl).
        setParameter("memberId", member.getId()).
        getSingleResult();
        return follower;
    }
}
public interface LiveConfigRepository extends CrudRepository<LiveConfig, Long> {
    @Query("SELECT lc FROM LiveConfig lc WHERE lc.custom_url =?1")
    LiveConfig findByCustomUrl(String customUrl);
    @Query("SELECT COUNT(lc) FROM LiveConfig lc WHERE lc.is_check = false")
    Long countByIsCheckFalse();

    @Modifying
    @Query("UPDATE LiveConfig lc SET lc.is_check = false")
    void setByIsCheckFalseAll();

    @Query("SELECT lc FROM LiveConfig lc WHERE lc.is_check = false")
    List<LiveConfig> findByIsCheckFalse();
}

이렇게 작성은 했는데 코드에 쿼리가 작성되는 것이 맞는건지는 잘 모르겠음

참고로 mybatis의 Mapper의 경우에도 비슷한 방식이 존재함

@Repository
public interface MemberRepository {
    Member save(Member member);
    Member findMember(String userid);
    List<Follower> findAllFollower(Long id);
}
public class MemberRepositoryV1 implements MemberRepository{

    @PersistenceContext
    EntityManager em;

    @Override
    public Member save(Member member) {
        em.persist(member);
        return member;
    }

    @Override
    public Member findMember(String userid) {
        try {
            Member member = (Member) em.createQuery("SELECT e FROM Member e WHERE e.username = :username", 
            Member.class).setParameter("username", userid).getSingleResult();
            return member;
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public List<Follower> findAllFollower(Long id) {
        Member member = em.find(Member.class, id);
        return member.getFollowers();
    }
}
public interface CheckYoutubeLiveStreamService {
    void checkYoutubeLiveStream() throws InterruptedException;
    void checkYoutubeLiveStreaminit() throws InterruptedException;
}

비동기로 하려고 하기 때문에 throws InterruptedException 를 추가함

 

@Service
public class CheckYoutubeLiveStreamServiceV1 implements CheckYoutubeLiveStreamService {
    private LiveConfigRepository liveConfigRepository;
    private ChromeOptions chromeOptions;

    public CheckYoutubeLiveStreamServiceV1(LiveConfigRepository liveConfigRepository) {
        this.liveConfigRepository = liveConfigRepository;
        chromeOptions = new ChromeOptions();
        chromeOptions.addArguments("--headless");
    }

    @Override
    public void checkYoutubeLiveStream() throws InterruptedException {
        List<LiveConfig> liveConfigs = liveConfigRepository.findByIsCheckFalse();
        if(liveConfigs.size() > 0){
            WebDriver driver = new ChromeDriver(chromeOptions);
            for (LiveConfig liveConfig : liveConfigs) {
                if(!liveConfig.is_check()){
                    driver.get("https://www.youtube.com/"+ liveConfig.getCustom_url() + "/streams");
                    if(isLiveStream(driver)){
                        setLiveConfigInfo(driver, liveConfig);
                    }else{
                        resetLiveConfigInfo(liveConfig);
                    }
                    liveConfigRepository.save(liveConfig);
                }
            }
            driver.quit();
        }
    }

    @Override
    public void checkYoutubeLiveStreaminit() throws InterruptedException {
        Long liveConfigs = liveConfigRepository.countByIsCheckFalse();
        Long cnt = liveConfigRepository.count();
        if(liveConfigs == 0 && cnt != 0){
            liveConfigRepository.setByIsCheckFalseAll();
        }
    }

    private boolean isLiveStream(WebDriver driver) {
        return driver.getPageSource().contains("overlay-style=\"LIVE\"") &&
               driver.getPageSource().contains("<div class=\"yt-tab-shape-wiz__tab yt-tab-shape-wiz__tab--tab-selected\">라이브</div>");
    }

    private void setLiveConfigInfo(WebDriver driver, LiveConfig liveConfig) {
        liveConfig.setIs_live("Live");
        liveConfig.setVideo_id(driver.getPageSource().split("is-live-video")[1].split("content")[0].split("watch?")[1].substring(3, 14));
        liveConfig.set_check(true);
    }

    private void resetLiveConfigInfo(LiveConfig liveConfig) {
        liveConfig.setIs_live("");
        liveConfig.setVideo_id("");
        liveConfig.set_check(true);
    }
}

public class CheckYoutubeLiveStreamServiceV2 implements CheckYoutubeLiveStreamService {

    private LiveConfigMapper liveConfigMapper;
    private ChromeOptions chromeOptions;

    public CheckYoutubeLiveStreamServiceV2(LiveConfigMapper liveConfigMapper) {
        this.liveConfigMapper = liveConfigMapper;
        chromeOptions = new ChromeOptions();
        chromeOptions.addArguments("--headless");
    }

    @Override
    public void checkYoutubeLiveStream() throws InterruptedException {
        int checkCount = liveConfigMapper.countByIsCheckFalse();
        if(checkCount > 0){
            List<LiveConfig> liveConfigs = liveConfigMapper.findByIsCheckFalse();
            WebDriver driver = new ChromeDriver(chromeOptions);
            for (LiveConfig liveConfig : liveConfigs) {
                if(!liveConfig.is_check()){
                    driver.get("https://www.youtube.com/"+ liveConfig.getCustom_url() + "/streams");
                    if(isLiveStream(driver)){
                        liveConfig = setLiveConfigInfo(driver, liveConfig);
                    }else{
                        liveConfig = resetLiveConfigInfo(liveConfig);
                    }
                    liveConfigMapper.updateLiveConfig(liveConfig);
                }
            }
            driver.quit();
        }
    }

    @Override
    public void checkYoutubeLiveStreaminit() throws InterruptedException {
        int checkCount = liveConfigMapper.countByIsCheckFalse();
        if(checkCount == 0){
            liveConfigMapper.setByIsCheckFalseAll();
        }
    }

    private boolean isLiveStream(WebDriver driver) {
        return driver.getPageSource().contains("overlay-style=\"LIVE\"") &&
               driver.getPageSource().contains("<div class=\"yt-tab-shape-wiz__tab yt-tab-shape-wiz__tab--tab-selected\">라이브</div>");
    }

    private LiveConfig setLiveConfigInfo(WebDriver driver, LiveConfig liveConfig) {
        liveConfig.setIs_live("Live");
        liveConfig.setVideo_id(driver.getPageSource().split("is-live-video")[1].split("content")[0].split("watch?")[1].substring(3, 14));
        liveConfig.set_check(true);
        return liveConfig;
    }

    private LiveConfig resetLiveConfigInfo(LiveConfig liveConfig) {
        liveConfig.setIs_live("");
        liveConfig.setVideo_id("");
        liveConfig.set_check(true);
        return liveConfig;
    }
}

동작은 위와 동일

 

public interface MemberService {
    void join(Member member);
    boolean checkFollow(Member member, String customUrl);
    void follow(Member member, Follower follower);
    void setTwitchChannelId(Follower follower, String channelId);
    void unFollow(Member member, String follower);
    Member findUser(String username);
    Follower findFollower(Member member, String youtubeChannelId);
    LiveConfig searchChannel(String customUrl);
}
@Service
@Transactional
public class MemberServiceV1 implements MemberService{
    private MemberRepository memberRepository;
    private FollowerRepository followerRepository;
    private LiveConfigRepository liveConfigRepository;
    private SearchService searchService;

    public MemberServiceV1(MemberRepository memberRepository, FollowerRepository followerRepository, LiveConfigRepository liveConfigRepository, SearchService searchService) {
        this.memberRepository = memberRepository;
        this.followerRepository = followerRepository;
        this.liveConfigRepository = liveConfigRepository;
        this.searchService = searchService;
    }

    /**
     * @param member
     */
    @Override
    public void join(Member member){
        YoutubeSearchResult youtubeSearchResult;
        List<Follower> followerList = new ArrayList<>();
        String searchURLs = "";
        String nextPage = "";
        do{ 
            youtubeSearchResult = searchService.getYoutubeSubscribeList(member.getYoutubeChannelId(), "50", nextPage);
            searchURLs = "";
            for (Items result : youtubeSearchResult.getItems()) {
                if(result.getContentDetails().getSubscription() != null)
                    searchURLs += "&id=" + result.getContentDetails().getSubscription().getResourceId().getChannelId();
            }
            if(!searchURLs.equals("")){
                YoutubeChannelList youtubeChannelList = searchService.getChannelUrlList(searchURLs);
                for (YoutubeChannelList.Items result : youtubeChannelList.getItems()) {
                    Follower follower = new Follower();

                    LiveConfig liveConfig = Optional.ofNullable(liveConfigRepository.findByCustomUrl(result.getSnippet().getCustomUrl())).orElse(null);
                    if(liveConfig == null){
                        liveConfig = new LiveConfig();
                        liveConfig.setName(result.getSnippet().getTitle());
                        String description = result.getSnippet().getDescription();
                        if (description != null && description.length() > 100) {
                            liveConfig.setDescription(description.substring(0, 100));
                        } else {
                            liveConfig.setDescription(description);
                        }
                        liveConfig.setCustom_url(result.getSnippet().getCustomUrl());
                        liveConfig.setThumbnails_url(result.getSnippet().getThumbnails().getMedium().getUrl());
                    }
                    follower.setLiveConfig(liveConfig);
                    follower.setMember(member);
                    follower.setTwitch_channel_id("");
                    followerList.add(follower);
                }
                nextPage = Optional.ofNullable(youtubeSearchResult.getNextPageToken()).orElse("");
            }
        }while(!nextPage.equals(""));
        member.setFollowers(followerList);

        memberRepository.save(member);
    }

    /**
    * @param member
     * @param follower
     * @return boolean
     */
    @Override
    public boolean checkFollow(Member member, String customUrl){
        for (Follower follower : member.getFollowers()) {
                if (follower.getLiveConfig().getCustom_url().equals(customUrl)) {
                    return false;
            }
        }
        return true;
    }

    /**
     * @param member
     * @param follower
     */
    @Override
    public void follow(Member member, Follower follower){
        member.getFollowers().add(follower);
        memberRepository.save(member);
    }

    /**
     * @param member
     * @param follower
     */
    @Override
    public void setTwitchChannelId(Follower follower, String twitchChannelId) {
        follower.setTwitch_channel_id(twitchChannelId);
        followerRepository.saveFollower(follower);
    }

    /**
     * @param member
     * @param follower
     */
    @Override
    public void unFollow(Member member, String follower){
        followerRepository.deletFollower(followerRepository.findFollower(member, follower));
    }

    /**
     * @param id
     * @return Member
     */
    @Override
    public Member findUser(String username){
        Member member = memberRepository.findMember(username);
        return member;
    }

    /**
     * @param member
     * @param youtubeChannelId
     * @return Follower
     */
    @Override
    public Follower findFollower(Member member, String youtubeChannelId){
        return followerRepository.findFollower(member, youtubeChannelId);
    }

    /**
     * @param customUrl
     * @return LiveConfig
     */
    @Override
    public LiveConfig searchChannel(String customUrl){
        return Optional.ofNullable(liveConfigRepository.findByCustomUrl(customUrl)).orElse(null);
    }
}

 

public class MemberServiceV2 implements MemberService{
    private final FollowerMapper followerMapper;
    private final LiveConfigMapper liveConfigMapper;
    private final MemberMapper memberMapper;
    private final SearchService searchService;

    public MemberServiceV2(FollowerMapper followerMapper, LiveConfigMapper liveConfigMapper, MemberMapper memberMapper, SearchService searchService) {
        this.followerMapper = followerMapper;
        this.liveConfigMapper = liveConfigMapper;
        this.memberMapper = memberMapper;
        this.searchService = searchService;
    }

    @Override
    public void join(Member member) {
        memberMapper.save(member);
        YoutubeSearchResult youtubeSearchResult;
        Long memberId = memberMapper.findMember(member.getUsername()).getId();
        String searchURLs = "";
        String nextPage = "";
        do{ 
            youtubeSearchResult = searchService.getYoutubeSubscribeList(member.getYoutubeChannelId(), "50", nextPage);
            searchURLs = "";
            for (Items result : youtubeSearchResult.getItems()) {
                if(result.getContentDetails().getSubscription() != null)
                    searchURLs += "&id=" + result.getContentDetails().getSubscription().getResourceId().getChannelId();
            }
            if(!searchURLs.equals("")){
                YoutubeChannelList youtubeChannelList = searchService.getChannelUrlList(searchURLs);
                for (YoutubeChannelList.Items result : youtubeChannelList.getItems()) {
                    LiveConfig liveConfig = Optional.ofNullable(liveConfigMapper.findByCustomUrl(result.getSnippet().getCustomUrl())).orElse(null);
                    if(liveConfig == null){
                        liveConfig = new LiveConfig();
                        liveConfig.setName(result.getSnippet().getTitle());
                        String description = result.getSnippet().getDescription();
                        if (description != null && description.length() > 100) {
                            liveConfig.setDescription(description.substring(0, 100));
                        } else {
                            liveConfig.setDescription(description);
                        }
                        liveConfig.setCustom_url(result.getSnippet().getCustomUrl());
                        liveConfig.setThumbnails_url(result.getSnippet().getThumbnails().getMedium().getUrl());
                        liveConfig.setVideo_id("");
                        liveConfig.setIs_live("");
                        liveConfigMapper.save(liveConfig);
                    }
                    followerMapper.save(memberId, liveConfig.getCustom_url());
                }
                nextPage = Optional.ofNullable(youtubeSearchResult.getNextPageToken()).orElse("");
            }
        }while(!nextPage.equals(""));
    }

    @Override
    public boolean checkFollow(Member member, String customUrl) {
        for (Follower follower : member.getFollowers()) {
            if (follower.getLiveConfig().getCustom_url().equals(customUrl)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public void follow(Member member, Follower follower) {
        followerMapper.save(member.getId(), follower.getLiveConfig().getCustom_url());
    }

    @Override
    public void setTwitchChannelId(Follower follower, String twitchChannelId) {
        followerMapper.updateTwitchChannelId(twitchChannelId, follower.getId(), follower.getLiveConfig().getCustom_url());
    }

    @Override
    public void unFollow(Member member, String follower) {
        followerMapper.delete(member, follower);
    }

    @Override
    public Member findUser(String username) {
        Member member = Optional.ofNullable(memberMapper.findMember(username)).orElse(null);
        if(member == null){
            return null;
        }
        List<String> urlList = followerMapper.findAllFollowerByMemberId(member.getId());
        List<Follower> followers = setFollowers(member, urlList);
        member.setFollowers(followers);

        return member;
    }

    @Override
    public Follower findFollower(Member member, String youtubeChannelId) {
        return followerMapper.findFollower(member, youtubeChannelId);
    }

    @Override
    public LiveConfig searchChannel(String customUrl) {
        return liveConfigMapper.findByCustomUrl(customUrl);
    }

    public List<Follower> setFollowers(Member member, List<String> urlList) {
        List<Follower> followers = new ArrayList<Follower>();
        for(String url : urlList) {
            Follower follower = new Follower();
            follower.setId(member.getId());
            follower.setLiveConfig(liveConfigMapper.findByCustomUrl(url));
            followers.add(follower);
        }
        return followers;
    }
}
public interface SearchService {
    YoutubeSearchResult youtubeSearchChannel(String query);
    YoutubeSearchResult getYoutubeSubscribeList(String youtubeChannelId, String maxResults, String pageToken);
    YoutubeChannelList getChannelUrlList(String youtubeChannelIds);
    TwitchSearchResult twitchSearchChannel(String query);
}
public class SearchServiceV1 implements SearchService {
    @Value("${youtube.api.key}")
    private String youtubeApiKey;
    @Value("${twitch.api.client.id}")
    private String twitchClientId;
    @Value("${twitch.api.client.acces_token}")
    private String twitchStringToken;

    @Override
    public YoutubeSearchResult youtubeSearchChannel(String query) {
        ObjectMapper objectMapper = new ObjectMapper();

        try {
            String encodedQuery = URLEncoder.encode(query, "UTF-8");
            String url = "https://youtube.googleapis.com/youtube/v3/search?part=snippet&channelType=any&maxResults=25&q=" + encodedQuery + "&type=channel&key=" + youtubeApiKey; // JSON 데이터를 가져올 URL을 지정합니다.
            HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
            connection.setRequestMethod("GET");

            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
                String inputLine;
                StringBuffer response = new StringBuffer();

                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }
                in.close();

                String jsonResponse = response.toString();
                YoutubeSearchResult searchResult = objectMapper.readValue(jsonResponse, YoutubeSearchResult.class);

                return searchResult;
            } else {
                System.out.println("HTTP 요청 실패: " + responseCode);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new YoutubeSearchResult();
    }

    @Override
    public YoutubeSearchResult getYoutubeSubscribeList(String youtubeChannelId, String maxResults, String pageToken) {
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            String url = "https://www.googleapis.com/youtube/v3/activities?part=snippet%2CcontentDetails"
                    +"&channelId=" + youtubeChannelId
                    +"&maxResults="+ maxResults
                    +"&pageToken="+ pageToken
                    + "&key=" + youtubeApiKey;
            HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
            connection.setRequestMethod("GET");

            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
                String inputLine;
                StringBuffer response = new StringBuffer();

                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }
                in.close();

                String jsonResponse = response.toString();
                YoutubeSearchResult searchResult = objectMapper.readValue(jsonResponse, YoutubeSearchResult.class);
                return searchResult;
            } else {
                System.out.println("HTTP 요청 실패: " + responseCode);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new YoutubeSearchResult();
    }

    @Override
    public YoutubeChannelList getChannelUrlList(String youtubeChannelIds) {
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            String url = "https://www.googleapis.com/youtube/v3/channels?part=snippet&maxResults=50"
                    + youtubeChannelIds + "&key=" + youtubeApiKey;;
            HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
            connection.setRequestMethod("GET");

            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
                String inputLine;
                StringBuffer response = new StringBuffer();

                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }
                in.close();

                String jsonResponse = response.toString();
                YoutubeChannelList searchResult = objectMapper.readValue(jsonResponse, YoutubeChannelList.class);
                return searchResult;
            } else {
                System.out.println("HTTP 요청 실패: " + responseCode);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new YoutubeChannelList();
    }

    @Override
    public TwitchSearchResult twitchSearchChannel(String query) {
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            String encodedQuery = URLEncoder.encode(query, "UTF-8");
            String url = "https://api.twitch.tv/helix/search/channels?query=" + encodedQuery; // JSON 데이터를 가져올 URL을 지정합니다.
            HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
            connection.setRequestMethod("GET");
            connection.setRequestProperty("Authorization", "Bearer " + twitchStringToken);
            connection.setRequestProperty("Client-Id", twitchClientId);

            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
                String inputLine;
                StringBuffer response = new StringBuffer();

                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }
                in.close();

                String jsonResponse = response.toString();
                TwitchSearchResult TwitchSearchResult = objectMapper.readValue(jsonResponse, TwitchSearchResult.class);
                return TwitchSearchResult;
            } else {
                System.out.println("HTTP 요청 실패: " + responseCode);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new TwitchSearchResult();
    }
}
@Controller
@CrossOrigin(origins = "${cors.origins}", allowCredentials = "true")
public class BroadcastSearchController {
    public final MemberService memberService;
    public final SearchService searchService;
    public final SessionManager sessionManager;

    public BroadcastSearchController(MemberService memberService, SessionManager sessionManager, SearchService searchService) {
        this.memberService = memberService;
        this.sessionManager = sessionManager;
        this.searchService = searchService;
    }

    @GetMapping("/follow")
    @ResponseBody
    public void follow(Model model, 
        @RequestParam("customUrl") String customUrl,
        HttpServletRequest request
    ){
        Member member = (Member)sessionManager.getSession(request);
        LiveConfig liveConfig = memberService.searchChannel(customUrl);
        // liveConfig 가 존재하지 않은 경우에는 동작하지 않는다.
        // 해당의 경우에는 에러를 보내거나 하는것이 옳을것으로 보인다.
        // 따로 안내를 해야할 것 같다. => Refresh 함수 같은것이 필요로 할것으로 보인다.
        if(member != null && liveConfig!= null){
            member = memberService.findUser(member.getUsername());
            if(memberService.checkFollow(member, customUrl)){
                Follower follower = new Follower();
                follower.setLiveConfig(liveConfig);
                follower.setMember(member);

                memberService.follow(member, follower);
            }else{
                // 중복이 있는경우 이므로 조치를 해야한다.
            }
        }
    }

    @GetMapping("/unFollow")
    @ResponseBody
    public void unFollow(Model model, 
        @RequestParam("userId") String userId, 
        @RequestParam("youtubeChannelId") String youtubeChannelId,
        HttpServletRequest request
    ){
        if(sessionManager.getSession(request) != null){
            Member member = memberService.findUser(userId);
            memberService.unFollow(member, youtubeChannelId);
        }
    }

    @GetMapping("/searchChannel")
    @ResponseBody
    public SearchResult searchChannel(Model model, 
        @RequestParam("search") String customUrl, 
        HttpServletRequest request
    ){  
        SearchResult searchResult = new SearchResult();
        if(sessionManager.getSession(request) != null){
            LiveConfig liveConfig = memberService.searchChannel(customUrl);
            searchResult.setLiveConfig(liveConfig);
            if(liveConfig != null) searchResult.setSearchResult(true);
            else searchResult.setSearchResult(false);
            return searchResult;
        }
        searchResult.setSearchResult(false);
        return searchResult;
    }

    @GetMapping("/getFollowChannels")
    @ResponseBody
    public List<LiveConfig> getFollowChannels(Model model, 
        HttpServletRequest request
    ){  
        Member member = (Member)sessionManager.getSession(request);
        if(member != null){
            member = memberService.findUser(member.getUsername());
            List<Follower> followers = member.getFollowers();
            List<LiveConfig> configs = new ArrayList<>();
            for(Follower follower : followers){
                configs.add(follower.getLiveConfig());
            }
            return configs;
        }
        return new ArrayList<LiveConfig>();
    }

    @GetMapping("/getLiveStreamTwitchChannelId")
    @ResponseBody
    public String getLiveStreamTwitchChannelId(Model model, 
        @RequestParam("custumUrl") String customUrl, 
        HttpServletRequest request
    ){  
        Member member = (Member)sessionManager.getSession(request);
        if(member != null){
            member = memberService.findUser(member.getUsername());
            LiveConfig liveConfig = memberService.searchChannel(customUrl);
            List<Follower> followers = member.getFollowers();
            for(Follower follower : followers){
                if(follower.getLiveConfig().equals(liveConfig)){
                    return follower.getTwitch_channel_id();
                }
            }
        }
        return "";
    }

    @GetMapping("/setLiveStreamTwitchChannelId")
    @ResponseBody
    public boolean setLiveStreamTwitchChannelId(Model model, 
        @RequestParam("custumUrl") String custumUrl, 
        @RequestParam("twitchChannelId") String twitchChannelId, 
        HttpServletRequest request
    ){  
        Member member = (Member)sessionManager.getSession(request);
        if(member != null){
            member = memberService.findUser(member.getUsername());
            LiveConfig liveConfig = memberService.searchChannel(custumUrl);
            List<Follower> followers = member.getFollowers();
            for(Follower follower : followers){
                if(follower.getLiveConfig().equals(liveConfig)){
                    follower.setTwitch_channel_id(twitchChannelId);
                    memberService.setTwitchChannelId(follower, twitchChannelId);
                    return true;
                }
            }
        }
        return false;
    }

    @GetMapping("/twitchSearchChannel")
    @ResponseBody
    public TwitchSearchResult twitchSearchChannel(Model model, @RequestParam("search") String query, 
        HttpServletRequest request) {
        if(sessionManager.getSession(request)!= null){
            TwitchSearchResult twitchSearchResult = searchService.twitchSearchChannel(query);
            return twitchSearchResult;
        }
        return new TwitchSearchResult();
    }
}
@Controller
@CrossOrigin(origins = "${cors.origins}", allowCredentials = "true")
public class HomeController {
    
    public final MemberService memberService;
    public final SessionManager sessionManager;

    public HomeController(MemberService memberService , SessionManager sessionManager){
        this.memberService = memberService;
        this.sessionManager = sessionManager;
    }

    @PostMapping("/signup")
    @ResponseBody
    public String signUp(@RequestBody SignupRequest request){
        Member member = new Member();
        member.setUsername(request.getUsername());
        member.setPassword(request.getPassword());
        member.setYoutubeChannelId(request.getYoutubeChannelId());
        
        // 중복 체크
        if(memberService.findUser(member.getUsername()) != null){
            return "false";
        }
        // youtube channel id check 가 필요하다.
        memberService.join(member);
        return "true";
    }

    @PostMapping("/login")
    @ResponseBody
    public String login(@RequestBody LoginRequest request, HttpServletResponse response){
        LoginRequest loginRequest = new LoginRequest();
        loginRequest.setUsername(request.getUsername());  //username
        loginRequest.setPassword(request.getPassword());  //password
        Member member;
        // send session cookie
        member = memberService.findUser(loginRequest.getUsername());

        if(member!= null && member.getPassword().equals(loginRequest.getPassword())){
            sessionManager.createSession(member, response);
            return "true";
        }
        return "false";
    }

    @GetMapping("/logout")
    @ResponseBody
    public boolean logout(HttpServletRequest request){
        sessionManager.expire(request);
        return true;
    }

    @GetMapping("/checkLogin")
    @ResponseBody
    public boolean checkCookies(Model model, HttpServletRequest request){
        if(sessionManager.getSession(request) != null){
            return true;    
        }
        return false;
    }
}
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig  {
    @Bean(name = "checkYoutubeLiveStream")
    public TaskExecutor checkYoutubeLiveStreamConfig() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2); // 기본 스레드 수
        executor.setMaxPoolSize(8); // 최대 스레드 수
        executor.setQueueCapacity(10000); // 큐 크기
        executor.setThreadNamePrefix("checkYoutubeLiveStream");
        executor.initialize();
        return executor;
    }

    @Bean(name = "checkYoutubeLiveStreamInit")
    public TaskExecutor checkYoutubeLiveStreamInitConfig() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1); // 기본 스레드 수
        executor.setMaxPoolSize(1); // 최대 스레드 수
        executor.setQueueCapacity(10000); // 큐 크기
        executor.setThreadNamePrefix("checkYoutubeLiveStreamInit");
        executor.initialize();
        return executor;
    }
}

 

@Configuration
@MapperScan("space.personal.mappers")
public class MybatisConfig {
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
        return sessionFactoryBean.getObject();
    }

    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

 

@SpringBootApplication
public class PersonalApplication {

	public static void main(String[] args) {
		SpringApplication.run(PersonalApplication.class, args);
	}

	@PostConstruct
	public void init() {
        // Chrome 웹 드라이버 경로를 설정
        System.setProperty("webdriver.chrome.driver", "src/main/resources/chromedriver.exe");
    }

}

셀레니움을 사용하기 때문에 드라이브를 서버 실행후 세팅되게 설정

@Configuration
public class SchedulingConfig {
    private final CheckYoutubeLiveStreamService checkYoutubeLiveStreamService;
    
    public SchedulingConfig(CheckYoutubeLiveStreamService checkYoutubeLiveStreamService){
        this.checkYoutubeLiveStreamService = checkYoutubeLiveStreamService;
    }

    @Async("checkYoutubeLiveStream")
    @Scheduled(fixedRate = 5000) 
    public void checkYoutubeLiveStream() throws InterruptedException{
        checkYoutubeLiveStreamService.checkYoutubeLiveStream();
    }

    @Async("checkYoutubeLiveStreamInit")
    @Scheduled(fixedRate = 60000)
    public void checkYoutubeLiveStreaminit() throws InterruptedException{
        checkYoutubeLiveStreamService.checkYoutubeLiveStreaminit();
    }
}

youtube라이브 체크는 5초마다 실행하도록 설정했고

데이터베이스에 존재하는 체크포인트를 검사하는건 1분으로 설정해 두었습니다.

이렇게 하면 전체적으로 전부 체크를 했는지 검사하는건 1분마다 검사하고 라이브 체크하는건 5초간격으로 검사하기 때문에 검사 속도가 빨라졌습니다.

 

public class ServletInitializer extends SpringBootServletInitializer {

	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
		return application.sources(PersonalApplication.class);
	}

}

외부 tomcat으로 배포를 해보고 싶어서 SpringBootServletInitializer를 사용해 보았다.

 

@Component
public class SessionManager {
    public static final String SESSION_COOKIE_NAME = "PIViewerSessionCookie";
    private Map<String, Object> sessionStore = new ConcurrentHashMap<>();

    public void createSession(Object value, HttpServletResponse response) {
        String sessionId = UUID.randomUUID().toString();
        sessionStore.put(sessionId, value);
        Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
        response.addCookie(mySessionCookie);
    }

    public Object getSession(HttpServletRequest request) {
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie == null) {
            return null;
        }
        return sessionStore.get(sessionCookie.getValue());
    }

    public void expire(HttpServletRequest request){
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie == null) {
            return;
        }
        sessionStore.remove(sessionCookie.getValue());
    }

    public Cookie findCookie(HttpServletRequest request, String cookieName) {
        if(request.getCookies() == null){
            return null;
        }
        return Arrays.stream(request.getCookies())
                .filter(cookie -> cookie.getName().equals(cookieName))
                .findAny()
                .orElse(null);
    }
}

다중 서버를 운영할 목적은 아니기 때문에 서버내의 세션을 구성해 보았습니다.

 

@Configuration
public class SpringConfig {
    private final FollowerMapper followerMapper;
    private final LiveConfigMapper liveConfigMapper;
    private final MemberMapper memberMapper;
    
    public SpringConfig(FollowerMapper followerMapper, LiveConfigMapper liveConfigMapper, MemberMapper memberMapper){
        this.followerMapper = followerMapper;
        this.liveConfigMapper = liveConfigMapper;
        this.memberMapper = memberMapper;
    }

    @Bean
    public MemberRepository memberRepository(){
        return new MemberRepositoryV1();
    }
    @Bean
    public FollowerRepository followerRepository(){
        return new FollowerRepositoryV1();
    }
    @Bean
    public MemberService memberService(){
        return new MemberServiceV2(
            followerMapper,
            liveConfigMapper,
            memberMapper,
            searchService());
    }

    @Bean
    public CheckYoutubeLiveStreamService checkYoutubeLiveStreamService(){
        return new CheckYoutubeLiveStreamServiceV2(liveConfigMapper);
    }
    
    @Bean
    public SearchService searchService(){
        return new SearchServiceV1();
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="space.personal.mappers.FollowerMapper">
    <insert id="save" parameterType="map">
        INSERT INTO follower (member_id, custom_url) VALUES (#{memberId}, #{customUrl})
    </insert>

    <delete id="delete" parameterType="map">
        DELETE FROM follower WHERE member_id = #{member.id} AND custom_url = #{customUrl}
    </delete>

    <update id="updateTwitchChannelId" parameterType="map">
        UPDATE follower 
        SET twitch_channel_id = #{twitch_channel_id} 
        WHERE follower.member_id = #{member_id} AND 
            custom_url = #{custom_url}
    </update>

    <select id="findFollower" parameterType="map" resultType="Follower">
        SELECT * FROM follower WHERE member_id = #{member.id} AND custom_url = #{customUrl}
    </select>

    <select id="findAllFollowerByMemberId" parameterType="Long" resultType="String">
        SELECT custom_url FROM follower WHERE member_id = #{memberId}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="space.personal.mappers.LiveConfigMapper">
    <insert id="save" parameterType="LiveConfig">
        INSERT INTO live_config (name, custom_url, description, thumbnails_url, is_check, is_live, video_id) 
        VALUES (#{name}, #{custom_url}, #{description}, #{thumbnails_url}, 0, #{is_live}, #{video_id})
    </insert>

    <update id="updateLiveConfig" parameterType="LiveConfig">
        UPDATE live_config 
        SET is_live = #{is_live},
            video_id = #{video_id},
            is_check = #{is_check}
        WHERE id = #{id}
    </update>

    <update id="setByIsCheckFalseAll">
        UPDATE live_config SET is_check = FALSE
    </update>

    <delete id="delete" parameterType="LiveConfig">
        DELETE FROM live_config WHERE name = #{name}
    </delete>

    <select id="findByCustomUrl" parameterType="String" resultType="LiveConfig">
        SELECT * FROM live_config WHERE custom_url = #{custom_url}
    </select>

    <select id="existsByCustomUrl" resultType="boolean" parameterType="String">
        SELECT COUNT(*)
        FROM follower
        WHERE custom_url = #{custom_url}
    </select>

    <select id="countByIsCheckFalse" resultType="int">
        SELECT COUNT(*) FROM live_config WHERE is_check = FALSE
    </select>

    <select id="findAll" resultType="LiveConfig">
        SELECT * FROM live_config
    </select>

    <select id="findByIsCheckFalse" resultType="LiveConfig">
        SELECT * FROM live_config WHERE is_check = FALSE
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="space.personal.mappers.MemberMapper">
    <insert id="save" parameterType="Member">
        INSERT INTO member (username, password, youtube_channel_id) VALUES (#{username}, #{password}, #{youtubeChannelId})
    </insert>

    <delete id="delete" parameterType="Member">
        DELETE FROM member WHERE username = #{username}
    </delete>

    <select id="findMember" resultType="Member">
        SELECT * FROM member WHERE username = #{username}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="application.properties" />
    <settings>
        <setting name="lazyLoadingEnabled" value="true"/>
    </settings>
    <typeAliases>
        <package name="space.personal.domain"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="UNPOOLED">
                <property name="driver" value="${spring.datasource.driver-class-name}"/>
                <property name="url" value="${spring.datasource.url}"/>
                <property name="username" value="${spring.datasource.username}"/>
                <property name="password" value=""/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mappers/FollowerMapper.xml"/>
        <mapper resource="mappers/MemberMapper.xml"/>
        <mapper resource="mappers/LiveConfigMapper.xml"/>
    </mappers>
</configuration>

이상으로 프로젝트에서 구성한 백엔드의 소스 코드였습니다.

개발하면서 생긴 문제점들이 상당히 많지만 특별히 기억나는 문제점은

V1을 개발할때 member를 비롯한 entity를 구성할때 습관적으로 카멜케이스로 변수명을 작성했었는데 spring에서 자동으로 생성할때 스네이크 표기법으로 입력이 되는 것 때문에 mybatis를 추가하는 과정에서 entity를 구성하는 모든곳을 스네이크 표기법으로 바꿔야 했습니다.

최대한 추상화라던지 하드코딩을 하지 않기 위해서 노력했습니다.

보안 요소 같은 요소가 추가되지 않고

회원가입, 로그인, 방송 시청( 채팅 검색 및 활성화 ) 와 같이 기본적인 기능들을 구현하였습니다.

 

properties의 대한 정보는 생략했습니다.

반응형
반응형

외부 api를 이용해서 기능을 구현해볼까 싶어서 youtube api와 twitch api 두개다 테스트를 해보니 문제없이 작동을 해서 팔로우 동작 방향을

와 같이 구성해볼까 싶어서 동작을 해보았는데

구성을 하고 테스트를 진행하다 보니 문제가 생겼다.

테스트 도중에 갑자기 테스트 진행이 원활하게 되지 않는 상황이 발생했는데 알아보니까

 

지금 보는것은 할당량이 초기화가 되어서 전부 0%로 표기 되어 있지만

하루 할당량이 100%로 전부 사용하여서 API이용기 불가해 졌던 것입니다.

할당량이 1만인데 대체 어떤 방식이길래 갑자기 100% 그것도 짧은 기간 사용했는데 100% 된것에 당황해서 왜 그렇게 된것인지 알아보니

 

YouTube Data API (v3) - 할당량 계산기  |  Google for Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English YouTube Data API (v3) - 할당량 계산기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 아래 표

developers.google.com

이라는게 있어서 살펴보니

검색 1건당 무려 100의 할당량을 사용한다는 것을 알게 되었습니다.

 

팔로우 등록뿐만 아니라 라이브 상황을 검색하기 위해서 검색이라는 기능을 계속 사용하려고 했는데

그냥 등록하기 위해 검색하는것만으로도 하루 100번 밖에 검색을 하지 못하기 때문에 계획했던 방식으로 설계하는것은 무리인것을 확인 그럼 할당량을 적게 사용하면서 비슷한 기능을 구현하기 위해서는 어떻게 사용해야 하는지 다시한번 고려해보니

을 활용하면 유저의 채널 ID만 있으면 구독한 채널에 대한 정보를 가져오는것이 가능하고 그렇게 되면

이런 흐름으로 진행한다면 일단 회원가입을 하는 사용자의 채널에 구독된 채널에 대한 정보를 얻을수 있고 얻은 정보를 바탕으로 검색으로 얻었어야 했던 정보를 얻을수 있다.


그러고 나서 처음에는 원래라면 youtube API Search를 통해서 

해당 매개변수를 이용해서 출력되는

"liveBroadcastContent": string

로 라이브 여부를 판단할 계획이었으나 해당 부분이 무산된 관계로 다른 방법을 고민을 한 결과

와 같이 https://www.youtube.com/@sbsnews8/streams 와 같이 https://www.youtube.com/<커스텀 url>/streams 로 url이 구성되어있는것을 확인할수 있었다.

그래서 Selenium을 이용해서 스크래핑을 해서 이미지에 표시되어 있는 <실시간>에 해당되는 html이 존재하는지 확인이 가능하면서 비디오를 출력하기 위한 video_id도 추출도 가능하기에 이런 흐름을 이용해서 라이브 상태를 파악하고자 한다.

해당 기능은 구독한 채널들의 일부 조건이 필요하다.

라이브를 하는건 단 1개만 라이브를 진행한다.(일부 뉴스의 경우 여러개를 실행하는 경우가 존재하지만 일반적인 방송인 경우 1개를 넘어가는 경우가 없기 때문)

 

반응형
반응형

만들었던 디자인에 따라서 HTML 만들어 보았다.

 

헤더는 고정값에 사이드바와 콘텐츠 영역은 해당 헤드보다 밑으로 구성하게 만들고 싶었다.

그렇기 때문에 css를

.header{
  position: fixed; /* 헤더를 화면 상단에 고정 */
  height: 70px;
  width: 100%; /* 가로폭을 화면 전체로 확장 */
  z-index: 1; /* 다른 요소 위에 나타나도록 설정 (사이드바 아래) */
}

와 같이 설정을 하여서 고정 값으로 높이를 설정하고

 

사이브바와 컨텐츠 영역에는

.mt-header{
  margin-top: 70px;
}

 

와 같이 margin top을 헤더영역과 같이 적용을 해서 헤더와 밑에 다른 영역과의 침범하지 못하게 만들어 보았습니다.

 

결과)

반응형

'토이프로젝트' 카테고리의 다른 글

토이프로젝트 - PIViewer(4) backend  (0) 2023.11.27
토이프로젝트 - PIViewer(3)  (0) 2023.11.12
토이프로젝트 - PIViewer(1)  (0) 2023.11.10
Twitch API 간단 사용해보기  (0) 2023.11.10
youtube API 사용해보기  (0) 2023.10.30
반응형

youtube api와 twitch api을 사용하여 높은 화질의 youtube의 영상과 빠른 반응의 twitch 채팅을 동시에 가져와서 보다 편안한 방송 시청을 하고자 이 프로젝트를 진행하고자 합니다.

 

 

 

Flowchart Maker & Online Diagram Software

Flowchart Maker and Online Diagram Software draw.io is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool, to design database schema, to build BPMN online, as a circuit d

app.diagrams.net

간단히 다이어 그램을 작성할수 있는 툴을 이용해서 작성하였습니다.

 

프로젝트 요구사항 파악

  • 시청자(이용자)는 회원 비회원으로 구분된다.
  • 시청자는 즐겨찾기 등록 방송공지 조회 방송 조회 방송 시청기능을 사용할 수 있다.
  • 시청자는 모든 기능을 사용하려면 반드시 로그인을 수행해야한다.
  • 방송 공지 조회는 공지 확인 시스템을 통해 조회가 된다.
  • 방송 조회는 Live 확인시스템을 통해 조회가 된다.
  • 즐겨찾기 등록은 팔로우 검색 시스템을 통해 조회가 된다.
  • 비 회원은 회원가입기능을 사용할 수 있다.

UI 설계

 

OvenApp.io

Oven(오븐)은 HTML5 기반의 무료 웹/앱 프로토타이핑 툴입니다. (카카오 제공)

ovenapp.io

https://ovenapp.io/view/ZZbB2my7Y1unl289aZtYk4lVgn7obvof/FzN7L

 

UI설계는 간단하게 oven을 활용해서 진행하였습니다.

figma 와 같은 프로그램도 분명 존재하지만 일단 협업을 하는 프로젝트가 아니고 개인으로 하는 프로젝트이기 때문에 최대한 가벼운 툴을 사용하고자 했기 때문에 OvenApp을 선택하게 되었습니다.

 

프로젝트 활용(할) 기술

Backend  
Java Version  java 17.0.8 2023-07-18 LTS
SQL Mapper 마이바티스
ORM 하이버네이
Spring Spring 부트 3.1.5
  Spring WEB MVC
Build Gradle
Frontend Javascript
  React
  Bootstrap
IDE VScode
DataBase H2(개발시)
  maria DB

 

반응형

'토이프로젝트' 카테고리의 다른 글

토이프로젝트 - PIViewer(4) backend  (0) 2023.11.27
토이프로젝트 - PIViewer(3)  (0) 2023.11.12
토이프로젝트 - PIViewer(2)  (0) 2023.11.10
Twitch API 간단 사용해보기  (0) 2023.11.10
youtube API 사용해보기  (0) 2023.10.30

+ Recent posts